mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-31 21:41:33 +00:00
Merge branch 'dejal' of https://github.com/samuelclay/NewsBlur into dejal
# Conflicts: # clients/ios/Resources/MainInterface.storyboard
This commit is contained in:
commit
f77d8839ac
262 changed files with 53202 additions and 2321 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -19,6 +19,7 @@ certbot.conf
|
|||
task_env.py
|
||||
app_env.py
|
||||
data/
|
||||
api/ip_addresses.txt
|
||||
.prom_cache
|
||||
config/certificates
|
||||
**/*.xcuserstate
|
||||
|
@ -53,10 +54,12 @@ docker/haproxy/haproxy.consul.cfg
|
|||
docker/nginx/nginx.consul.conf
|
||||
docker/prometheus/prometheus.yml
|
||||
docker/redis/redis_replica.conf
|
||||
docker/redis/redis_*_replica.conf
|
||||
docker/postgres/postgres.conf
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
/originals
|
||||
/node/originals
|
||||
media/safari/NewsBlur.safariextz
|
||||
|
||||
# IDE files
|
||||
|
@ -66,3 +69,4 @@ media/safari/NewsBlur.safariextz
|
|||
*.tfstate*
|
||||
.terraform*
|
||||
grafana.ini
|
||||
apps/api/ip_addresses.txt
|
||||
|
|
34
.vscode/settings.json
vendored
34
.vscode/settings.json
vendored
|
@ -1,23 +1,16 @@
|
|||
{
|
||||
"black-formatter.args": [
|
||||
"--line-length 110"
|
||||
],
|
||||
"isort.args": [
|
||||
"--profile",
|
||||
"black"
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": false,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.pylamaEnabled": false,
|
||||
"python.linting.flake8Args": [
|
||||
"--ignore=E501,W293,W503,W504,E302,E722,E226,E221,E402,E401"
|
||||
],
|
||||
"python.pythonPath": "~/.virtualenvs/newsblur3/bin/python",
|
||||
// "python.linting.enabled": true,
|
||||
// "python.linting.pylintEnabled": false,
|
||||
// "python.linting.flake8Enabled": true,
|
||||
// "python.linting.pylamaEnabled": false,
|
||||
// "python.linting.flake8Args": [
|
||||
// "--ignore=E501,W293,W503,W504,E302,E722,E226,E221,E402,E401"
|
||||
// ],
|
||||
// "python.pythonPath": "~/.virtualenvs/newsblur/bin/python",
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": "active",
|
||||
"git.ignoreLimitWarning": true,
|
||||
|
@ -38,15 +31,12 @@
|
|||
"docker/volumes": true,
|
||||
"requirements.txt": true, // It's just a symlink to config/requirements.txt, which has git history
|
||||
},
|
||||
"python.formatting.blackArgs": [
|
||||
"--line-length=110",
|
||||
"--skip-string-normalization"
|
||||
],
|
||||
// "python.formatting.blackArgs": [
|
||||
// "--line-length=110",
|
||||
// "--skip-string-normalization"
|
||||
// ],
|
||||
"files.associations": {
|
||||
"*.yml": "ansible"
|
||||
},
|
||||
"nrf-connect.toolchain.path": "${nrf-connect.toolchain:1.9.1}",
|
||||
"C_Cpp.default.configurationProvider": "nrf-connect",
|
||||
"editor.formatOnSave": false,
|
||||
"ansible.python.interpreterPath": "/opt/homebrew/bin/python3",
|
||||
}
|
||||
|
|
2
Makefile
2
Makefile
|
@ -185,7 +185,7 @@ maintenance_off:
|
|||
|
||||
# Provision
|
||||
firewall:
|
||||
ansible-playbook ansible/all.yml -l db --tags firewall
|
||||
ansible-playbook ansible/all.yml -l db --tags ufw
|
||||
oldfirewall:
|
||||
ANSIBLE_CONFIG=/srv/newsblur/ansible.old.cfg ansible-playbook ansible/all.yml -l db --tags firewall
|
||||
repairmongo:
|
||||
|
|
|
@ -2,32 +2,48 @@ plugin: constructed
|
|||
strict: False
|
||||
|
||||
groups:
|
||||
|
||||
hall: inventory_hostname.startswith('h')
|
||||
|
||||
haproxy: inventory_hostname.startswith('hwww')
|
||||
|
||||
web: inventory_hostname.startswith('happ')
|
||||
app: inventory_hostname.startswith('happ')
|
||||
django: inventory_hostname.startswith('happ-django')
|
||||
happ: inventory_hostname.startswith('happ')
|
||||
web: inventory_hostname.startswith('happ')
|
||||
hweb: inventory_hostname.startswith('happ')
|
||||
django: inventory_hostname.startswith('happ-web')
|
||||
hdjango: inventory_hostname.startswith('happ-web')
|
||||
refresh: inventory_hostname.startswith('happ-refresh')
|
||||
counts: inventory_hostname.startswith('happ-counts')
|
||||
hrefresh: inventory_hostname.startswith('happ-refresh')
|
||||
count: inventory_hostname.startswith('happ-count')
|
||||
hcount: inventory_hostname.startswith('happ-count')
|
||||
push: inventory_hostname.startswith('happ-push')
|
||||
hpush: inventory_hostname.startswith('happ-push')
|
||||
blogs: inventory_hostname.startswith('blog')
|
||||
|
||||
node: inventory_hostname.startswith('hnode')
|
||||
hnode: inventory_hostname.startswith('hnode')
|
||||
node_socket: inventory_hostname.startswith('hnode-socket')
|
||||
hnode_socket: inventory_hostname.startswith('hnode-socket')
|
||||
node_images: inventory_hostname.startswith('hnode-images')
|
||||
hnode_images: inventory_hostname.startswith('hnode-images')
|
||||
node_text: inventory_hostname.startswith('hnode-text')
|
||||
hnode_text: inventory_hostname.startswith('hnode-text')
|
||||
node_page: inventory_hostname.startswith('hnode-page')
|
||||
hnode_page: inventory_hostname.startswith('hnode-page')
|
||||
node_favicons: inventory_hostname.startswith('hnode-favicons')
|
||||
hnode_favicons: inventory_hostname.startswith('hnode-favicons')
|
||||
|
||||
# debugs: inventory_hostname.startswith('hdebug')
|
||||
|
||||
htask: inventory_hostname.startswith('htask')
|
||||
task: inventory_hostname.startswith('htask')
|
||||
celery: inventory_hostname.startswith('htask-celery')
|
||||
work: inventory_hostname.startswith('htask-work')
|
||||
|
||||
staging: inventory_hostname.startswith('hstaging')
|
||||
|
||||
hdb: inventory_hostname.startswith('hdb')
|
||||
db: inventory_hostname.startswith('hdb')
|
||||
search: inventory_hostname.startswith('hdb-elasticsearch')
|
||||
elasticsearch: inventory_hostname.startswith('hdb-elasticsearch')
|
||||
|
@ -39,5 +55,6 @@ groups:
|
|||
mongo: inventory_hostname.startswith('hdb-mongo') and not inventory_hostname.startswith('hdb-mongo-analytics')
|
||||
mongo_analytics: inventory_hostname.startswith('hdb-mongo-analytics')
|
||||
consul: inventory_hostname.startswith('hdb-consul')
|
||||
hconsul: inventory_hostname.startswith('hdb-consul')
|
||||
metrics: inventory_hostname.startswith('hdb-metrics')
|
||||
sentry: inventory_hostname.startswith('hdb-sentry')
|
||||
|
|
|
@ -127,6 +127,9 @@
|
|||
- name: Reload gunicorn
|
||||
command: "kill -HUP {{ psaux.stdout }}"
|
||||
when: not pulled.changed
|
||||
rescue:
|
||||
- name: Restart Docker Container
|
||||
command: "docker restart newsblur_web"
|
||||
tags:
|
||||
- static
|
||||
|
||||
|
|
|
@ -6,16 +6,23 @@
|
|||
- ../env_vars/base.yml
|
||||
|
||||
tasks:
|
||||
- name: Extract part of hostname to determine container name
|
||||
set_fact:
|
||||
redis_role: "{{ inventory_hostname.split('-')[2] }}"
|
||||
tags:
|
||||
- never
|
||||
- replicaofnoone
|
||||
|
||||
- name: Turning off secondary for redis by deleting redis_replica.conf
|
||||
copy:
|
||||
dest: /srv/newsblur/docker/redis/redis_replica.conf
|
||||
dest: "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf"
|
||||
content: ""
|
||||
tags:
|
||||
- never
|
||||
- replicaofnoone
|
||||
|
||||
- name: Setting Redis REPLICAOF NO ONE
|
||||
shell: docker exec redis redis-cli REPLICAOF NO ONE
|
||||
shell: docker exec redis-{{ redis_role }} redis-cli REPLICAOF NO ONE
|
||||
tags:
|
||||
- never
|
||||
- replicaofnoone
|
||||
|
|
|
@ -8,16 +8,16 @@
|
|||
- motd_role: db
|
||||
|
||||
roles:
|
||||
- {role: 'base', tags: 'base'}
|
||||
- {role: 'ufw', tags: 'ufw'}
|
||||
- {role: 'docker', tags: 'docker'}
|
||||
- {role: 'repo', tags: ['repo', 'pull']}
|
||||
- {role: 'dnsmasq', tags: 'dnsmasq'}
|
||||
- {role: 'consul', tags: 'consul'}
|
||||
- {role: 'consul-client', tags: 'consul'}
|
||||
- {role: 'mongo-exporter', tags: 'mongo-exporter'}
|
||||
- {role: 'postgres-exporter', tags: 'postgres-exporter'}
|
||||
- {role: 'redis-exporter', tags: 'redis-exporter'}
|
||||
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
|
||||
- {role: 'prometheus', tags: ['prometheus', 'metrics']}
|
||||
- {role: 'grafana', tags: ['grafana', 'metrics']}
|
||||
# - {role: 'base', tags: 'base'}
|
||||
# - {role: 'ufw', tags: 'ufw'}
|
||||
# - {role: 'docker', tags: 'docker'}
|
||||
# - {role: 'repo', tags: ['repo', 'pull']}
|
||||
# - {role: 'dnsmasq', tags: 'dnsmasq'}
|
||||
# - {role: 'consul', tags: 'consul'}
|
||||
# - {role: 'consul-client', tags: 'consul'}
|
||||
# - {role: 'mongo-exporter', tags: 'mongo-exporter'}
|
||||
- { role: "postgres-exporter", tags: "postgres-exporter" }
|
||||
- { role: "redis-exporter", tags: "redis-exporter" }
|
||||
- { role: "node-exporter", tags: ["node-exporter", "metrics"] }
|
||||
- { role: "prometheus", tags: ["prometheus", "metrics"] }
|
||||
- { role: "grafana", tags: ["grafana", "metrics"] }
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
- name: SETUP -> node containers
|
||||
hosts: node
|
||||
become: true
|
||||
vars_files:
|
||||
- ../env_vars/base.yml
|
||||
vars:
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
- {role: 'docker', tags: 'docker'}
|
||||
- {role: 'repo', tags: ['repo', 'pull']}
|
||||
- {role: 'dnsmasq', tags: 'dnsmasq'}
|
||||
- {role: 'consul', tags: 'consul'}
|
||||
- {role: 'consul-client', tags: 'consul'}
|
||||
# - {role: 'consul', tags: 'consul'}
|
||||
# - {role: 'consul-client', tags: 'consul'}
|
||||
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
|
||||
- {role: 'redis', tags: 'redis'}
|
||||
- {role: 'flask_metrics', tags: ['flask-metrics', 'metrics', 'flask_metrics']}
|
||||
|
|
|
@ -23,5 +23,5 @@
|
|||
- {role: 'nginx', tags: 'nginx'}
|
||||
- {role: 'node', tags: 'node'}
|
||||
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
|
||||
- {role: 'prometheus', tags: ['prometheus', 'metrics']}
|
||||
- {role: 'grafana', tags: ['grafana', 'metrics']}
|
||||
# - {role: 'prometheus', tags: ['prometheus', 'metrics']}
|
||||
# - {role: 'grafana', tags: ['grafana', 'metrics']}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
- motd_role: task
|
||||
roles:
|
||||
- {role: 'base', tags: 'base'}
|
||||
# - {role: 'ufw', tags: 'ufw'}
|
||||
- {role: 'docker', tags: 'docker'}
|
||||
- {role: 'repo', tags: ['repo', 'pull']}
|
||||
- {role: 'dnsmasq', tags: 'dnsmasq'}
|
||||
|
@ -17,3 +16,4 @@
|
|||
- {role: 'apns', tags: 'apns'}
|
||||
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
|
||||
- {role: 'celery_task', tags: 'celery'}
|
||||
- {role: 'ufw', tags: 'ufw'}
|
||||
|
|
|
@ -103,14 +103,15 @@
|
|||
become: yes
|
||||
command:
|
||||
docker run --rm --name=pg_basebackup --network=host -e POSTGRES_PASSWORD=newsblur -v /srv/newsblur/docker/volumes/postgres/data:/var/lib/postgresql/data postgres:13 pg_basebackup -h db-postgres.service.nyc1.consul -p 5432 -U newsblur -D /var/lib/postgresql/data -Fp -R -Xs -P -c fast
|
||||
|
||||
- name: Create Postgres docker volumes with correct permissions
|
||||
become: yes
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
recurse: yes
|
||||
owner: "{{ ansible_effective_user_id|int }}"
|
||||
group: "{{ ansible_effective_group_id|int }}"
|
||||
owner: 999
|
||||
group: 999
|
||||
with_items:
|
||||
- /srv/newsblur/docker/volumes/postgres/archive
|
||||
- /srv/newsblur/docker/volumes/postgres/backups
|
||||
|
@ -129,7 +130,7 @@
|
|||
- name: pg_ctl promote
|
||||
become: yes
|
||||
command:
|
||||
docker exec -it postgres su - postgres -c "/usr/lib/postgresql/13/bin/pg_ctl -D /var/lib/postgresql/data promote"
|
||||
docker exec postgres su - postgres -c "/usr/lib/postgresql/13/bin/pg_ctl -D /var/lib/postgresql/data promote"
|
||||
# when: (inventory_hostname | regex_replace('[0-9]+', '')) in ['db-postgres-secondary']
|
||||
tags:
|
||||
- never
|
||||
|
|
|
@ -6,6 +6,17 @@
|
|||
tags: packages
|
||||
# ignore_errors: yes
|
||||
|
||||
- name: whoami
|
||||
debug:
|
||||
var: ansible_user_id
|
||||
tags: whoami
|
||||
|
||||
- name: Set timezone
|
||||
become: yes
|
||||
ansible.builtin.timezone:
|
||||
name: 'America/New_York'
|
||||
tags: timezone
|
||||
|
||||
- name: Copy zshrc
|
||||
template:
|
||||
src: zshrc.txt.j2
|
||||
|
@ -20,10 +31,20 @@
|
|||
become: yes
|
||||
|
||||
- name: Cloning oh-my-zsh
|
||||
git:
|
||||
repo: https://github.com/robbyrussell/oh-my-zsh
|
||||
dest: /home/nb/.oh-my-zsh
|
||||
force: yes
|
||||
block:
|
||||
- name: Cloning oh-my-zsh
|
||||
git:
|
||||
repo: https://github.com/robbyrussell/oh-my-zsh
|
||||
dest: /home/nb/.oh-my-zsh
|
||||
force: yes
|
||||
rescue:
|
||||
- name: chown oh-my-zsh
|
||||
become: yes
|
||||
file:
|
||||
path: /home/nb/.oh-my-zsh
|
||||
owner: nb
|
||||
group: nb
|
||||
recurse: yes
|
||||
|
||||
- name: Copy toprc
|
||||
copy: src=toprc.txt dest=~/.toprc
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
max-size: 100m
|
||||
healthcheck:
|
||||
# test: celery inspect ping -A newsblur_web -d celery@$HOSTNAME
|
||||
test: bash -c "(($(date +%s) - $(stat /srv/newsblur/logs/newsblur.log -c %Y) < 120)) && exit 0 || exit 1"
|
||||
test: bash -c "(($(date +%s) - $(stat /srv/newsblur/logs/newsblur.log -c %Y) < 600)) && exit 0 || exit 1"
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"log_file": "/var/log/consul/consul.log",
|
||||
"enable_syslog": true,
|
||||
"retry_join": [{{ consul_manager_ip.stdout|trim }}],
|
||||
{% if inventory_hostname.startswith("hdb") %}
|
||||
{% if inventory_hostname.startswith("hdb") and inventory_hostname|regex_replace("\-?\d+", "") not in ["hdb-mongo-analytics", "hdb-sentry"] %}
|
||||
"advertise_addr": "{% raw %}{{ GetAllInterfaces | include \"name\" \"^enp\" | include \"flags\" \"forwardable|up\" | attr \"address\" }}{% endraw %}",
|
||||
{% else %}
|
||||
"advertise_addr": "{% raw %}{{ GetAllInterfaces | include \"name\" \"^eth\" | include \"flags\" \"forwardable|up\" | attr \"address\" }}{% endraw %}",
|
||||
|
|
|
@ -1,12 +1,52 @@
|
|||
#!/usr/bin/env python
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import digitalocean
|
||||
|
||||
TOKEN_FILE = "/srv/secrets-newsblur/keys/digital_ocean.token"
|
||||
|
||||
OLD = False
|
||||
# Uncomment below to allow existing servers to find the consul-manager
|
||||
OLD = True
|
||||
def get_host_ips_from_group(group_name):
|
||||
"""
|
||||
Fetches IP addresses of hosts from a specified group using ansible-inventory command across combined inventory.
|
||||
|
||||
:param group_name: The name of the group to fetch host IPs from.
|
||||
:param inventory_base_path: Base path to the inventory directories. Defaults to the path in ansible.cfg.
|
||||
:return: A list of IP addresses belonging to the specified group.
|
||||
"""
|
||||
cmd = ['ansible-inventory', '-i', '/srv/newsblur/ansible/inventories/hetzner.ini', '-i', '/srv/newsblur/ansible/inventories/hetzner.yml', '--list']
|
||||
|
||||
try:
|
||||
# Execute the ansible-inventory command
|
||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
|
||||
|
||||
# Parse the JSON output from ansible-inventory
|
||||
inventory_data = json.loads(result.stdout)
|
||||
|
||||
host_ips = []
|
||||
# Check if the group exists
|
||||
if group_name in inventory_data:
|
||||
# Get the list of hosts in the specified group
|
||||
if 'hosts' in inventory_data[group_name]:
|
||||
for host in inventory_data[group_name]['hosts']:
|
||||
# Fetch the host details, specifically looking for the ansible_host variable for the IP
|
||||
host_vars = inventory_data['_meta']['hostvars'][host]
|
||||
ip_address = host_vars.get('ansible_host', None)
|
||||
if ip_address:
|
||||
host_ips.append(ip_address)
|
||||
else:
|
||||
# If ansible_host is not defined, fallback to using the host's name
|
||||
host_ips.append(host)
|
||||
return host_ips
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to execute ansible-inventory: {e.stderr}")
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Failed to parse JSON output: {e}")
|
||||
return []
|
||||
|
||||
|
||||
TOKEN_FILE = "/srv/secrets-newsblur/keys/digital_ocean.token"
|
||||
|
||||
with open(TOKEN_FILE) as f:
|
||||
token = f.read().strip()
|
||||
|
@ -14,11 +54,12 @@ with open(TOKEN_FILE) as f:
|
|||
|
||||
manager = digitalocean.Manager(token=token)
|
||||
my_droplets = manager.get_all_droplets()
|
||||
consul_manager_droplets = [d for d in my_droplets if d.name.startswith("db-consul")]
|
||||
if OLD:
|
||||
consul_manager_ip_address = ','.join([f"\"{droplet.ip_address}\"" for droplet in consul_manager_droplets])
|
||||
else:
|
||||
consul_manager_ip_address = ','.join([f"\"{droplet.private_ip_address}\"" for droplet in consul_manager_droplets])
|
||||
consul_manager_droplets = [d for d in my_droplets if "db-consul" in d.name]
|
||||
|
||||
# Use ansible-inventory to get the consul-manager ip
|
||||
group_name = 'hconsul'
|
||||
hetzner_hosts = get_host_ips_from_group(group_name)
|
||||
consul_manager_ip_address = ','.join([f"\"{droplet.ip_address}\"" for droplet in consul_manager_droplets] + [f"\"{host}\"" for host in hetzner_hosts])
|
||||
|
||||
print(consul_manager_ip_address)
|
||||
|
||||
|
|
|
@ -12,11 +12,21 @@
|
|||
apt_repository:
|
||||
repo: "deb [arch=amd64] https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main"
|
||||
|
||||
- name: Add the official HashiCorp Linux repository.
|
||||
become: yes
|
||||
apt_repository:
|
||||
repo: "deb [arch=arm64] https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main"
|
||||
|
||||
- name: Update apt
|
||||
become: yes
|
||||
apt:
|
||||
update_cache: yes
|
||||
|
||||
- name: Installing Consul
|
||||
become: yes
|
||||
apt:
|
||||
allow_downgrades: yes
|
||||
pkg: consul=1.10.4
|
||||
pkg: consul=1.10.12-1
|
||||
state: present
|
||||
|
||||
- name: Register Manager IP
|
||||
|
|
|
@ -80,14 +80,20 @@
|
|||
state: directory
|
||||
mode: 0755
|
||||
|
||||
- name: Add override for dnsmasq service
|
||||
become: yes
|
||||
copy:
|
||||
dest: /etc/systemd/system/dnsmasq.service.d/override.conf
|
||||
content: |
|
||||
[Unit]
|
||||
After=docker.service
|
||||
# - name: Add override for dnsmasq service
|
||||
# become: yes
|
||||
# copy:
|
||||
# dest: /etc/systemd/system/dnsmasq.service.d/override.conf
|
||||
# content: |
|
||||
# [Unit]
|
||||
# After=docker.service
|
||||
|
||||
- name: Remove override for dnsmasq service
|
||||
become: yes
|
||||
file:
|
||||
path: /etc/systemd/system/dnsmasq.service.d/override.conf
|
||||
state: absent
|
||||
|
||||
- name: Launch dnsmasq
|
||||
become: yes
|
||||
service:
|
||||
|
|
|
@ -9,12 +9,12 @@ server=/consul/127.0.0.1#8600
|
|||
no-resolv
|
||||
|
||||
{% for interface in network_interfaces %}
|
||||
{% if not interface.startswith('veth') %}
|
||||
interface={{ interface }}
|
||||
{% if not interface.startswith('veth') and not interface.startswith('docker') and not interface.startswith('br') %}
|
||||
# interface={{ interface }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
bind-interfaces
|
||||
# bind-interfaces # This will bind only to the interfaces that are up in interface= above
|
||||
# log-dhcp
|
||||
# log-queries
|
||||
# log-facility=/var/log/dnsmasq.log
|
||||
|
|
|
@ -15,25 +15,37 @@
|
|||
state: present
|
||||
with_items: "{{ docker_prerequisite_packages_Ubuntu }}"
|
||||
|
||||
- name: Install prerequisite packages (for Ubuntu 14.04 only)
|
||||
apt:
|
||||
name: "{{ item.package }}"
|
||||
state: present
|
||||
with_items: "{{ docker_prerequisite_packages_Ubuntu_1404 }}"
|
||||
when: ansible_distribution_version == '14.04'
|
||||
|
||||
- name: Import Docker CE repository gpg key
|
||||
- name: Download Docker GPG key
|
||||
become: yes
|
||||
apt_key:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
state: present
|
||||
id: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
|
||||
|
||||
- name: Add Docker CE repository
|
||||
shell:
|
||||
cmd: "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg"
|
||||
creates: /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
|
||||
- name: Set up the Docker repository with the correct GPG key
|
||||
become: yes
|
||||
apt_repository:
|
||||
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
ansible.builtin.apt_repository:
|
||||
repo: "deb [arch={{ ansible_architecture }} signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Set up the Docker repository with the correct GPG key
|
||||
become: yes
|
||||
ansible.builtin.apt_repository:
|
||||
repo: "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Set up the Docker repository with the correct GPG key
|
||||
become: yes
|
||||
ansible.builtin.apt_repository:
|
||||
repo: "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Update APT cache
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
update_cache: yes
|
||||
|
||||
- name: Install Docker CE
|
||||
become: yes
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
---
|
||||
- name: Set facts for secondary elasticsearch servers
|
||||
set_fact:
|
||||
elasticsearch_secondary: no
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Set facts for secondary elasticsearch servers
|
||||
set_fact:
|
||||
elasticsearch_secondary: yes
|
||||
# when: inventory_hostname not in ["db-elasticsearch"]
|
||||
when: inventory_hostname not in ["hdb-elasticsearch-1"]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Permissions for elasticsearch
|
||||
become: yes
|
||||
file:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"service": {
|
||||
{% if inventory_hostname in ["db-elasticsearch"] %}
|
||||
{% if not elasticsearch_secondary %}
|
||||
"name": "db-elasticsearch",
|
||||
{% else %}
|
||||
"name": "db-elasticsearch-staging",
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
- name: Start mongo-exporter container
|
||||
become: yes
|
||||
docker_container:
|
||||
|
@ -12,9 +11,9 @@
|
|||
- name: newsblurnet
|
||||
env:
|
||||
# MONGODB_URI: 'mongodb://{{ inventory_hostname }}.node.nyc1.consul:27017/admin?'
|
||||
MONGODB_URI: 'mongodb://{{ mongodb_username }}:{{ mongodb_password }}@{{ inventory_hostname }}.node.nyc1.consul:27017/admin?authSource=admin'
|
||||
MONGODB_URI: "mongodb://{{ mongodb_username }}:{{ mongodb_password }}@{{ inventory_hostname }}.node.nyc1.consul:27017/admin?authSource=admin"
|
||||
ports:
|
||||
- '9216:9216'
|
||||
- "9216:9216"
|
||||
|
||||
- name: Register mongo-exporter in consul
|
||||
tags: consul
|
||||
|
@ -24,5 +23,5 @@
|
|||
dest: /etc/consul.d/mongo-exporter.json
|
||||
notify:
|
||||
- reload consul
|
||||
- name: Command to register mongo-exporter
|
||||
command: "consul services register /etc/consul.d/mongo-exporter.json"
|
||||
# - name: Command to register mongo-exporter
|
||||
# command: "consul services register /etc/consul.d/mongo-exporter.json"
|
||||
|
|
|
@ -1,5 +1,22 @@
|
|||
---
|
||||
- name: Set mongo_analytics_secondary
|
||||
set_fact:
|
||||
mongo_analytics_secondary: no
|
||||
tags:
|
||||
- consul
|
||||
- always
|
||||
|
||||
- name: Set mongo_analytics_secondary
|
||||
set_fact:
|
||||
mongo_analytics_secondary: yes
|
||||
# when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) not in ['db-mongo-analytics']
|
||||
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) not in ['hdb-mongo-analytics']
|
||||
tags:
|
||||
- consul
|
||||
- always
|
||||
|
||||
- name: Permissions for mongo
|
||||
become: yes
|
||||
file:
|
||||
state: directory
|
||||
mode: 0755
|
||||
|
@ -8,7 +25,7 @@
|
|||
path: /var/log/mongodb
|
||||
|
||||
- name: Copy MongoDB keyfile
|
||||
# become: yes
|
||||
become: yes
|
||||
copy:
|
||||
content: "{{ mongodb_keyfile }}"
|
||||
dest: /srv/newsblur/config/mongodb_keyfile.key
|
||||
|
@ -226,7 +243,6 @@
|
|||
]
|
||||
}
|
||||
)' admin
|
||||
when: (inventory_hostname | regex_replace('[0-9]+', '')) == 'db-mongo-analytics'
|
||||
register: auth_result
|
||||
changed_when:
|
||||
- auth_result.rc == 0
|
||||
|
@ -258,7 +274,7 @@
|
|||
template:
|
||||
src: consul_service.analytics.json
|
||||
dest: /etc/consul.d/mongo.json
|
||||
when: (inventory_hostname | regex_replace('[0-9]+', '')) == 'db-mongo-analytics'
|
||||
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) in ['db-mongo-analytics', 'hdb-mongo-analytics']
|
||||
notify:
|
||||
- reload consul
|
||||
|
||||
|
@ -269,6 +285,7 @@
|
|||
- logrotate
|
||||
|
||||
- name: Add sanity checkers cronjob for disk usage
|
||||
become: yes
|
||||
cron:
|
||||
name: disk_usage_sanity_checker
|
||||
user: root
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"service": {
|
||||
"name": "db-mongo-analytics",
|
||||
{% if not mongo_analytics_secondary %}
|
||||
"name": "db-mongo-analytics",
|
||||
{% else %}
|
||||
"name": "db-mongo-analytics-secondary",
|
||||
{% endif %}
|
||||
"id": "{{ inventory_hostname }}",
|
||||
"tags": [
|
||||
"db"
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
---
|
||||
- name: Ensure 'nb' user owns /srv/ recursively
|
||||
become: yes
|
||||
file:
|
||||
path: /srv
|
||||
owner: nb
|
||||
group: nb
|
||||
recurse: yes
|
||||
state: directory
|
||||
|
||||
- name: Copy node secrets
|
||||
copy:
|
||||
src: /srv/secrets-newsblur/settings/node_settings.env
|
||||
|
@ -34,22 +43,22 @@
|
|||
- name: Get the volume name
|
||||
shell: ls /dev/disk/by-id/ | grep -v part
|
||||
register: volume_name_raw
|
||||
when: '"node-page" in inventory_hostname'
|
||||
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
|
||||
|
||||
- set_fact:
|
||||
volume_name: "{{ volume_name_raw.stdout }}"
|
||||
when: '"node-page" in inventory_hostname'
|
||||
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
|
||||
|
||||
- debug:
|
||||
msg: "{{ volume_name }}"
|
||||
when: '"node-page" in inventory_hostname'
|
||||
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
|
||||
|
||||
- name: Create the mount point
|
||||
become: yes
|
||||
file:
|
||||
path: "/mnt/{{ inventory_hostname | regex_replace('-', '') }}"
|
||||
state: directory
|
||||
when: '"node-page" in inventory_hostname'
|
||||
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
|
||||
|
||||
- name: Mount volume read-write
|
||||
become: yes
|
||||
|
@ -59,7 +68,7 @@
|
|||
fstype: xfs
|
||||
opts: defaults,discard
|
||||
state: mounted
|
||||
when: '"node-page" in inventory_hostname'
|
||||
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
|
||||
|
||||
- name: Symlink node-page volume from /srv/originals
|
||||
become: yes
|
||||
|
@ -67,10 +76,9 @@
|
|||
dest: /srv/originals
|
||||
src: "/mnt/{{ inventory_hostname | regex_replace('-', '') }}"
|
||||
state: link
|
||||
when: '"node-page" in inventory_hostname'
|
||||
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
|
||||
|
||||
- name: Start node docker containers
|
||||
become: yes
|
||||
docker_container:
|
||||
name: node
|
||||
image: newsblur/newsblur_node
|
||||
|
@ -104,7 +112,6 @@
|
|||
when: item in inventory_hostname
|
||||
|
||||
- name: Start non-newsblur node docker containers
|
||||
become: yes
|
||||
docker_container:
|
||||
name: "{{ item.container_name }}"
|
||||
image: "{{ item.image }}"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"service": {
|
||||
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}",
|
||||
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace('hnode', 'node') }}",
|
||||
"id": "{{ inventory_hostname }}",
|
||||
"tags": [
|
||||
"web",
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
---
|
||||
- name: Set facts for all postgres servers
|
||||
set_fact:
|
||||
postgres_secondary: no
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Set facts for secondary postgres servers
|
||||
set_fact:
|
||||
postgres_secondary: yes
|
||||
# when: inventory_hostname not in ["db-postgres2"]
|
||||
when: inventory_hostname not in ["hdb-postgres-1"]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Template postgresql-13.conf file
|
||||
template:
|
||||
src: /srv/newsblur/docker/postgres/postgresql-13.conf.j2
|
||||
|
@ -91,7 +105,7 @@
|
|||
- /srv/newsblur/docker/postgres/postgres_ident-13.conf:/etc/postgresql/pg_ident.conf
|
||||
- /home/nb/.ssh/id_rsa:/var/lib/postgresql/.ssh/id_rsa
|
||||
restart_policy: unless-stopped
|
||||
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) in ['db-postgres-primary', 'db-postgres', 'hdb-postgres']
|
||||
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) in ['db-postgres-primary', 'db-postgres', 'hdb-postgres', 'hdb-postgres-secondary']
|
||||
|
||||
# - name: Change ownership in postgres docker container
|
||||
# become: yes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"service": {
|
||||
{% if inventory_hostname.startswith('db-postgres2') %}
|
||||
{% if not postgres_secondary %}
|
||||
"name": "db-postgres",
|
||||
{% else %}
|
||||
"name": "db-postgres-secondary",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
restart_policy: unless-stopped
|
||||
container_default_behavior: no_defaults
|
||||
env:
|
||||
REDIS_ADDR: "db-{{item.redis_target}}.service.nyc1.consul:6379"
|
||||
REDIS_ADDR: "db-{{item.redis_target}}.service.nyc1.consul:{{ item.redis_port }}"
|
||||
networks_cli_compatible: yes
|
||||
network_mode: default
|
||||
networks:
|
||||
|
@ -16,12 +16,16 @@
|
|||
with_items:
|
||||
- port: 9121
|
||||
redis_target: "redis-user"
|
||||
redis_port: 6381
|
||||
- port: 9122
|
||||
redis_target: "redis-sessions"
|
||||
redis_target: "redis-session"
|
||||
redis_port: 6382
|
||||
- port: 9123
|
||||
redis_target: "redis-story"
|
||||
redis_port: 6380
|
||||
- port: 9124
|
||||
redis_target: "redis-pubsub"
|
||||
redis_port: 6383
|
||||
|
||||
- name: Register redis-exporters in consul
|
||||
tags: consul
|
||||
|
@ -35,7 +39,7 @@
|
|||
- port: 9121
|
||||
redis_target: "redis-user"
|
||||
- port: 9122
|
||||
redis_target: "redis-sessions"
|
||||
redis_target: "redis-session"
|
||||
- port: 9123
|
||||
redis_target: "redis-story"
|
||||
- port: 9124
|
||||
|
|
|
@ -8,7 +8,19 @@
|
|||
state: reloaded
|
||||
listen: reload consul
|
||||
|
||||
- name: restart redis
|
||||
- name: restart redis user
|
||||
become: yes
|
||||
command: docker restart redis
|
||||
listen: restart redis
|
||||
command: docker restart redis-user
|
||||
listen: restart redis_user
|
||||
- name: restart redis story
|
||||
become: yes
|
||||
command: docker restart redis-story
|
||||
listen: restart redis_story
|
||||
- name: restart redis session
|
||||
become: yes
|
||||
command: docker restart redis-session
|
||||
listen: restart redis_session
|
||||
- name: restart redis pubsub
|
||||
become: yes
|
||||
command: docker restart redis-pubsub
|
||||
listen: restart redis_pubsub
|
||||
|
|
|
@ -1,4 +1,31 @@
|
|||
---
|
||||
- name: Extract part of hostname to determine container name
|
||||
set_fact:
|
||||
redis_role: "{{ inventory_hostname.split('-')[2] }}"
|
||||
redis_secondary: no
|
||||
# redis_port: 6379
|
||||
redis_ports:
|
||||
story: 6380
|
||||
user: 6381
|
||||
session: 6382
|
||||
pubsub: 6383
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Set redis_port for redis servers
|
||||
set_fact:
|
||||
redis_port: "{{ redis_ports[redis_role] }}"
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Set redis_secondary for secondary redis servers
|
||||
set_fact:
|
||||
redis_secondary: yes
|
||||
# when: inventory_hostname not in ["db-redis-user", "db-redis-story1", "db-redis-session", "db-redis-pubsub"]
|
||||
when: inventory_hostname not in ["hdb-redis-user-1", "hdb-redis-story-1", "hdb-redis-session-1", "hdb-redis-pubsub"]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Install sysfsutils for disabling transparent huge pages
|
||||
become: yes
|
||||
package:
|
||||
|
@ -25,16 +52,22 @@
|
|||
copy:
|
||||
src: /srv/newsblur/docker/redis/redis.conf
|
||||
dest: /srv/newsblur/docker/redis/redis.conf
|
||||
notify: restart redis
|
||||
notify: "restart redis_{{ redis_role }}"
|
||||
register: updated_config
|
||||
|
||||
- name: Template redis_replica.conf file
|
||||
template:
|
||||
src: /srv/newsblur/docker/redis/redis_replica.conf.j2
|
||||
dest: /srv/newsblur/docker/redis/redis_replica.conf
|
||||
notify: restart redis
|
||||
dest: "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf"
|
||||
notify: "restart redis_{{ redis_role }}"
|
||||
register: updated_config
|
||||
when: "'db-redis-story2' not in inventory_hostname"
|
||||
when: redis_secondary
|
||||
|
||||
- name: Remove redis_replica.conf file if not secondary
|
||||
copy:
|
||||
dest: "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf"
|
||||
content: ""
|
||||
when: not redis_secondary
|
||||
|
||||
- name: Create Redis docker volume directory
|
||||
file:
|
||||
|
@ -46,7 +79,7 @@
|
|||
|
||||
- name: Create Redis docker volume with correct permissions
|
||||
file:
|
||||
path: /srv/newsblur/docker/volumes/redis
|
||||
path: "/srv/newsblur/docker/volumes/redis_{{ redis_role }}"
|
||||
state: directory
|
||||
recurse: yes
|
||||
owner: "{{ ansible_effective_user_id|int }}"
|
||||
|
@ -54,7 +87,7 @@
|
|||
|
||||
- name: Start redis docker containers
|
||||
docker_container:
|
||||
name: redis
|
||||
name: "redis-{{ redis_role }}"
|
||||
image: redis:7
|
||||
pull: true
|
||||
state: started
|
||||
|
@ -69,19 +102,19 @@
|
|||
aliases:
|
||||
- redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
- "{{ redis_port }}:6379"
|
||||
restart_policy: unless-stopped
|
||||
user: "{{ ansible_effective_user_id|int }}:{{ ansible_effective_group_id|int }}"
|
||||
volumes:
|
||||
- /srv/newsblur/docker/volumes/redis:/data
|
||||
- /srv/newsblur/docker/redis/redis.conf:/usr/local/etc/redis/redis_server.conf
|
||||
- /srv/newsblur/docker/redis/redis_replica.conf:/usr/local/etc/redis/redis_replica.conf
|
||||
- "/srv/newsblur/docker/volumes/redis_{{ redis_role }}:/data"
|
||||
- "/srv/newsblur/docker/redis/redis.conf:/usr/local/etc/redis/redis_server.conf"
|
||||
- "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf:/usr/local/etc/redis/redis_replica.conf"
|
||||
|
||||
- name: Register redis in consul
|
||||
become: yes
|
||||
template:
|
||||
src: consul_service.json
|
||||
dest: /etc/consul.d/redis.json
|
||||
dest: "/etc/consul.d/redis_{{ redis_role }}.json"
|
||||
notify:
|
||||
- reload consul
|
||||
tags: consul
|
||||
|
@ -130,7 +163,7 @@
|
|||
job: >
|
||||
docker run --rm
|
||||
-v /srv/newsblur:/srv/newsblur
|
||||
-v /srv/newsblur/docker/volumes/redis/dump.rdb:/data/dump.rdb
|
||||
-v /srv/newsblur/docker/volumes/redis_{{ redis_role }}/dump.rdb:/data/dump.rdb
|
||||
--network=newsblurnet
|
||||
--hostname={{ ansible_hostname }}
|
||||
newsblur/newsblur_python3 python /srv/newsblur/utils/backups/backup_redis.py
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
{
|
||||
"service": {
|
||||
{% if inventory_hostname in ["db-redis-user", "db-redis-story1", "db-redis-session", "db-redis-pubsub"] %}
|
||||
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}",
|
||||
{% if not redis_secondary %}
|
||||
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace("hdb", "db") }}",
|
||||
{% else %}
|
||||
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace('hdb-', 'db-') }}-staging",
|
||||
"name": "db-redis-{{ redis_role }}-staging",
|
||||
{% endif %}
|
||||
"id": "{{ inventory_hostname }}",
|
||||
"tags": [
|
||||
"redis"
|
||||
],
|
||||
"port": 6379,
|
||||
"port": {{ redis_port }},
|
||||
"checks": [{
|
||||
"id": "{{inventory_hostname}}-ping",
|
||||
{% if 'db-redis-story' in inventory_hostname %}
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_story?consul=1",
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_story?consul=1&port={{ redis_port }}",
|
||||
{% elif 'db-redis-user' in inventory_hostname %}
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_user?consul=1",
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_user?consul=1&port={{ redis_port }}",
|
||||
{% elif 'db-redis-pubsub' in inventory_hostname %}
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_pubsub?consul=1",
|
||||
{% elif 'db-redis-sessions' in inventory_hostname %}
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_sessions?consul=1",
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_pubsub?consul=1&port={{ redis_port }}",
|
||||
{% elif 'db-redis-session' in inventory_hostname %}
|
||||
"http": "http://{{ ansible_host }}:5579/db_check/redis_sessions?consul=1&port={{ redis_port }}",
|
||||
{% else %}
|
||||
"http": "http://{{ ansible_host }}:5000/db_check/redis?consul=1",
|
||||
{% endif %}
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
---
|
||||
- name: Ensure /srv exists and is owned by user
|
||||
become: yes
|
||||
file:
|
||||
path: /srv
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: 0755
|
||||
|
||||
- name: Pull sentry self-hosted github
|
||||
git:
|
||||
repo: https://github.com/getsentry/self-hosted.git
|
||||
dest: /srv/sentry/
|
||||
version: master
|
||||
version: 24.4.1
|
||||
|
||||
- name: Updating Sentry
|
||||
command:
|
||||
chdir: /srv/sentry/
|
||||
cmd: ./install.sh
|
||||
cmd: ./install.sh --no-report-self-hosted-issues
|
||||
|
||||
- name: docker-compuse up -d
|
||||
command:
|
||||
|
@ -24,4 +33,3 @@
|
|||
notify:
|
||||
- reload consul
|
||||
when: disable_consul_services_ie_staging is not defined
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"service": {
|
||||
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}",
|
||||
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace("hdb-", "db-") }}",
|
||||
"id": "{{ inventory_hostname }}",
|
||||
"tags": [
|
||||
"sentry"
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
---
|
||||
- name: Stop ufw and delete all rules
|
||||
become: yes
|
||||
ufw: state=reset
|
||||
tags: ufw
|
||||
|
||||
- name: Set firewall default policy
|
||||
- name: Set hosts
|
||||
set_fact:
|
||||
hetzner_hosts: "{{ groups['hall'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
do_hosts: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
|
||||
- name: Generate UFW batch script
|
||||
become: yes
|
||||
ufw: state=disabled policy=reject
|
||||
tags: ufw
|
||||
template:
|
||||
src: ufw_rules.sh.j2
|
||||
dest: /tmp/ufw_rules.sh
|
||||
mode: '0755'
|
||||
|
||||
# - name: Stop ufw and delete all rules
|
||||
# become: yes
|
||||
# ufw: state=reset
|
||||
# tags: ufw
|
||||
|
||||
# - name: Set firewall default policy
|
||||
# become: yes
|
||||
# ufw: state=disabled policy=reject
|
||||
# tags: ufw
|
||||
#
|
||||
# - name: Set ufw policy to deny all incoming connections
|
||||
# ufw: policy=deny direction=incoming
|
||||
|
@ -16,81 +29,87 @@
|
|||
# - name: Set ufw policy to allow all ougoing connections
|
||||
# ufw: policy=allow direction=outgoing
|
||||
# tags: ufw
|
||||
|
||||
- name: Execute UFW batch script
|
||||
become: yes
|
||||
command: /tmp/ufw_rules.sh
|
||||
|
||||
# - name: Allow ssh
|
||||
# become: yes
|
||||
# ufw: rule=allow port=22
|
||||
# tags: ufw
|
||||
|
||||
# - name: Allow all access from RFC1918 networks to this host
|
||||
# become: yes
|
||||
# ufw:
|
||||
# rule: allow
|
||||
# src: '{{ item }}'
|
||||
# with_items:
|
||||
# - 10.0.0.0/8
|
||||
# - 172.18.0.0/16
|
||||
# - 172.17.0.0/16
|
||||
# tags:
|
||||
# - firewall
|
||||
# - ufw
|
||||
|
||||
- name: Allow ssh
|
||||
become: yes
|
||||
ufw: rule=allow port=22
|
||||
tags: ufw
|
||||
# - name: Allow all access from Hetzner inventory hosts
|
||||
# become: yes
|
||||
# ufw:
|
||||
# rule: allow
|
||||
# src: '{{ item }}'
|
||||
# with_items: "{{ groups['hall'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
# tags:
|
||||
# - firewall
|
||||
# - ufw
|
||||
# - hetzner_firewall
|
||||
# - hfirewall
|
||||
|
||||
- name: Allow all access from RFC1918 networks to this host
|
||||
become: yes
|
||||
ufw:
|
||||
rule: allow
|
||||
src: '{{ item }}'
|
||||
with_items:
|
||||
- 10.0.0.0/8
|
||||
- 172.18.0.0/16
|
||||
- 172.17.0.0/16
|
||||
tags:
|
||||
- firewall
|
||||
- ufw
|
||||
|
||||
- name: Allow all access from Hetzner inventory hosts
|
||||
become: yes
|
||||
ufw:
|
||||
rule: allow
|
||||
src: '{{ item }}'
|
||||
with_items: "{{ groups['NewsBlur_Hetzner'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
tags:
|
||||
- firewall
|
||||
- ufw
|
||||
- hetzner_firewall
|
||||
# - name: Allow all access from Hetzner inventory hosts with docker
|
||||
# become: yes
|
||||
# ufw:
|
||||
# rule: allow
|
||||
# route: yes
|
||||
# src: '{{ item }}'
|
||||
# with_items: "{{ groups['hall'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
# tags:
|
||||
# - firewall
|
||||
# - ufw
|
||||
# - hetzner_firewall
|
||||
# - hfirewall
|
||||
|
||||
- name: Allow all access from Hetzner inventory hosts with docker
|
||||
become: yes
|
||||
ufw:
|
||||
rule: allow
|
||||
route: yes
|
||||
src: '{{ item }}'
|
||||
with_items: "{{ groups['NewsBlur_Hetzner'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
tags:
|
||||
- firewall
|
||||
- ufw
|
||||
- hetzner_firewall
|
||||
# - name: Allow all access from inventory hosts old + new
|
||||
# become: yes
|
||||
# ufw:
|
||||
# rule: allow
|
||||
# src: '{{ item }}'
|
||||
# with_items: "{{ groups['oldandnew'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
# when: "'oldandnew' in groups"
|
||||
# tags:
|
||||
# - firewall
|
||||
# - ufw
|
||||
|
||||
- name: Allow all access from inventory hosts old + new
|
||||
become: yes
|
||||
ufw:
|
||||
rule: allow
|
||||
src: '{{ item }}'
|
||||
with_items: "{{ groups['oldandnew'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
when: "'oldandnew' in groups"
|
||||
tags:
|
||||
- firewall
|
||||
- ufw
|
||||
# - name: Allow all access from inventory hosts
|
||||
# become: yes
|
||||
# ufw:
|
||||
# rule: allow
|
||||
# src: '{{ item }}'
|
||||
# with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
# when: "'oldandnew' not in groups"
|
||||
# tags:
|
||||
# - firewall
|
||||
# - ufw
|
||||
|
||||
- name: Allow all access from inventory hosts
|
||||
become: yes
|
||||
ufw:
|
||||
rule: allow
|
||||
src: '{{ item }}'
|
||||
with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
when: "'oldandnew' not in groups"
|
||||
tags:
|
||||
- firewall
|
||||
- ufw
|
||||
|
||||
- name: Allow all access from inventory hosts with docker
|
||||
become: yes
|
||||
ufw:
|
||||
rule: allow
|
||||
route: yes
|
||||
src: '{{ item }}'
|
||||
with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
when: "'oldandnew' not in groups"
|
||||
tags:
|
||||
- firewall
|
||||
- ufw
|
||||
# - name: Allow all access from inventory hosts with docker
|
||||
# become: yes
|
||||
# ufw:
|
||||
# rule: allow
|
||||
# route: yes
|
||||
# src: '{{ item }}'
|
||||
# with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
|
||||
# when: "'oldandnew' not in groups"
|
||||
# tags:
|
||||
# - firewall
|
||||
# - ufw
|
||||
|
||||
# Code from https://stackoverflow.com/a/51741599/8717: "What is the best practice of docker + ufw under Ubuntu"
|
||||
- name: Solving UFW and Docker issues by adding ufw after.rules
|
||||
|
|
43
ansible/roles/ufw/templates/ufw_rules.sh.j2
Normal file
43
ansible/roles/ufw/templates/ufw_rules.sh.j2
Normal file
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
# Script to apply UFW rules incrementally with regex matching for `ufw status verbose` output
|
||||
|
||||
# Fetch the current UFW status once and store it
|
||||
CURRENT_UFW_STATUS=$(ufw status verbose)
|
||||
|
||||
# Function to check and apply a rule with regex for forward rules
|
||||
apply_rule() {
|
||||
local rule="$1"
|
||||
local rule_type="$2" # "IN" for incoming, "FWD" for forwarded (route) rules
|
||||
local ip_address="$3"
|
||||
|
||||
# Construct the regex pattern based on rule type
|
||||
local regex_pattern
|
||||
if [ "$rule_type" == "FWD" ]; then
|
||||
regex_pattern="ALLOW FWD\\s+$ip_address"
|
||||
else
|
||||
regex_pattern="ALLOW IN\\s+$ip_address"
|
||||
fi
|
||||
|
||||
# Use grep with -P for Perl-compatible regex, and -q to quietly check for a match
|
||||
if echo "$CURRENT_UFW_STATUS" | grep -Pq -- "$regex_pattern"; then
|
||||
echo "Rule already exists: $regex_pattern"
|
||||
else
|
||||
echo "Applying rule: $rule"
|
||||
ufw $rule
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply rules
|
||||
# Example for direct allow rules
|
||||
apply_rule "allow 22" "IN" "22" # IP address parameter is not used for this rule
|
||||
|
||||
# Example for forwarded (route) rules
|
||||
{% for host in hetzner_hosts %}
|
||||
apply_rule "allow from {{ host }}" "IN" "{{ host }}"
|
||||
apply_rule "route allow from {{ host }}" "FWD" "{{ host }}"
|
||||
{% endfor %}
|
||||
|
||||
{% for host in do_hosts %}
|
||||
apply_rule "allow from {{ host }}" "IN" "{{ host }}"
|
||||
apply_rule "route allow from {{ host }}" "FWD" "{{ host }}"
|
||||
{% endfor %}
|
|
@ -29,6 +29,14 @@
|
|||
tags:
|
||||
- static
|
||||
|
||||
- name: "Write out ips from ansible inventory of all servers with app/node/www/task in name into addresses.txt file"
|
||||
template:
|
||||
src: ip_addresses.txt.j2
|
||||
dest: /srv/newsblur/apps/api/ip_addresses.txt
|
||||
tags:
|
||||
- inventory
|
||||
|
||||
|
||||
- name: Prune docker
|
||||
become: yes
|
||||
community.docker.docker_prune:
|
||||
|
|
5
ansible/roles/web/templates/ip_addresses.txt.j2
Normal file
5
ansible/roles/web/templates/ip_addresses.txt.j2
Normal file
|
@ -0,0 +1,5 @@
|
|||
{%- for item in hostvars %}
|
||||
{% if "happ" in item or "hnode" in item or "hwww" in item or "htask" in item %}
|
||||
{{ hostvars[item].ansible_host }}
|
||||
{% endif %}
|
||||
{%- endfor %}
|
|
@ -26,4 +26,4 @@
|
|||
- import_playbook: playbooks/setup_metrics.yml
|
||||
when: "'metrics' in inventory_hostname"
|
||||
- import_playbook: playbooks/setup_sentry.yml
|
||||
when: "'sentry' in inventory_hostname"
|
||||
when: "'sentry' in inventory_hostname or 'metrics' in inventory_hostname"
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
import os
|
||||
import base64
|
||||
import urllib.parse
|
||||
import datetime
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
import lxml.html
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth import login as login_user
|
||||
from django.contrib.auth import logout as logout_user
|
||||
from apps.reader.forms import SignupForm, LoginForm
|
||||
from django.core.mail import mail_admins
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
from apps.profile.models import Profile
|
||||
from apps.social.models import MSocialProfile, MSharedStory, MSocialSubscription
|
||||
from apps.rss_feeds.models import Feed, MStarredStoryCounts, MStarredStory
|
||||
from apps.reader.forms import LoginForm, SignupForm
|
||||
from apps.reader.models import RUserStory, UserSubscription, UserSubscriptionFolders
|
||||
from apps.rss_feeds.models import Feed, MStarredStory, MStarredStoryCounts
|
||||
from apps.rss_feeds.text_importer import TextImporter
|
||||
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory
|
||||
from apps.social.models import MSharedStory, MSocialProfile, MSocialSubscription
|
||||
from utils import json_functions as json
|
||||
from utils import log as logging
|
||||
from utils.feed_functions import relative_timesince
|
||||
from utils.user_functions import ajax_login_required, get_user
|
||||
from utils.view_functions import required_params
|
||||
from utils.user_functions import get_user, ajax_login_required
|
||||
|
||||
|
||||
@json.json_view
|
||||
|
@ -511,8 +514,11 @@ def save_story(request, token=None):
|
|||
return response
|
||||
|
||||
def ip_addresses(request):
|
||||
import digitalocean
|
||||
doapi = digitalocean.Manager(token=settings.DO_TOKEN_API_IPADDRESSES)
|
||||
droplets = doapi.get_all_droplets()
|
||||
addresses = '\n'.join([d.ip_address for d in droplets if any(name in d.name for name in ['task', 'staging', 'app', 'node'])])
|
||||
# Read local file /srv/newsblur/apps/api/ip_addresses.txt and return that
|
||||
with open('/srv/newsblur/apps/api/ip_addresses.txt', 'r') as f:
|
||||
addresses = f.read()
|
||||
|
||||
if request.user.is_authenticated:
|
||||
mail_admins(f"IP Addresses accessed from {request.META['REMOTE_ADDR']} by {request.user}", addresses)
|
||||
|
||||
return HttpResponse(addresses, content_type='text/plain')
|
||||
|
|
|
@ -24,7 +24,7 @@ from utils.view_functions import is_true
|
|||
from utils.story_functions import truncate_chars
|
||||
from utils import log as logging
|
||||
from utils import mongoengine_fields
|
||||
from apns2.errors import BadDeviceToken, Unregistered
|
||||
from apns2.errors import BadDeviceToken, Unregistered, DeviceTokenNotForTopic
|
||||
from apns2.client import APNsClient
|
||||
from apns2.payload import Payload
|
||||
from bs4 import BeautifulSoup
|
||||
|
@ -306,14 +306,15 @@ class MUserFeedNotification(mongo.Document):
|
|||
|
||||
tokens = MUserNotificationTokens.get_tokens_for_user(self.user_id)
|
||||
# To update APNS:
|
||||
# 0. Upgrade to latest openssl: brew install openssl
|
||||
# 1. Create certificate signing request in Keychain Access
|
||||
# 2. Upload to https://developer.apple.com/account/resources/certificates/list
|
||||
# 3. Download to secrets/certificates/ios/aps.cer
|
||||
# 4. Open in Keychain Access:
|
||||
# 4. Open in Keychain Access, Under "My Certificates":
|
||||
# - export "Apple Push Service: com.newsblur.NewsBlur" as aps.p12 (or just use aps.cer in #5)
|
||||
# - export private key as aps_key.p12 WITH A PASSPHRASE (removed later)
|
||||
# 5. openssl x509 -in aps.cer -inform DER -out aps.pem -outform PEM
|
||||
# 6. openssl pkcs12 -nocerts -out aps_key.pem -in aps_key.p12
|
||||
# 6. openssl pkcs12 -in aps_key.p12 -out aps_key.pem -nodes -legacy
|
||||
# 7. openssl rsa -out aps_key.noenc.pem -in aps_key.pem
|
||||
# 7. cat aps.pem aps_key.noenc.pem > aps.p12.pem
|
||||
# 8. Verify: openssl s_client -connect gateway.push.apple.com:2195 -cert aps.p12.pem
|
||||
|
@ -348,7 +349,7 @@ class MUserFeedNotification(mongo.Document):
|
|||
)
|
||||
try:
|
||||
apns.send_notification(token, payload, topic="com.newsblur.NewsBlur")
|
||||
except (BadDeviceToken, Unregistered):
|
||||
except (BadDeviceToken, Unregistered, DeviceTokenNotForTopic):
|
||||
logging.user(user, '~BMiOS token expired: ~FR~SB%s' % (token[:50]))
|
||||
else:
|
||||
confirmed_ios_tokens.append(token)
|
||||
|
|
|
@ -1598,7 +1598,7 @@ class Profile(models.Model):
|
|||
self.save()
|
||||
|
||||
delta = datetime.datetime.now() - self.last_seen_on
|
||||
months_ago = delta.days / 30
|
||||
months_ago = round(delta.days / 30)
|
||||
user = self.user
|
||||
data = dict(user=user, months_ago=months_ago)
|
||||
text = render_to_string('mail/email_premium_expire_grace.txt', data)
|
||||
|
@ -1627,7 +1627,7 @@ class Profile(models.Model):
|
|||
return
|
||||
|
||||
delta = datetime.datetime.now() - self.last_seen_on
|
||||
months_ago = delta.days / 30
|
||||
months_ago = round(delta.days / 30)
|
||||
user = self.user
|
||||
data = dict(user=user, months_ago=months_ago)
|
||||
text = render_to_string('mail/email_premium_expire.txt', data)
|
||||
|
|
|
@ -1,73 +1,114 @@
|
|||
import base64
|
||||
import concurrent
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import zlib
|
||||
|
||||
import redis
|
||||
import requests
|
||||
import random
|
||||
import zlib
|
||||
import concurrent
|
||||
import re
|
||||
import ssl
|
||||
import socket
|
||||
import base64
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.template.loader import render_to_string
|
||||
from django.db import IntegrityError
|
||||
from django.db.utils import DatabaseError
|
||||
from django.db.models import Q
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import login as login_user
|
||||
from django.contrib.auth import logout as logout_user
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404, UnreadablePostError
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.core.validators import validate_email
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.db.utils import DatabaseError
|
||||
from django.http import (
|
||||
Http404,
|
||||
HttpResponse,
|
||||
HttpResponseForbidden,
|
||||
HttpResponseRedirect,
|
||||
UnreadablePostError,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import feedgenerator
|
||||
from django.utils.encoding import smart_str
|
||||
from mongoengine.queryset import OperationError
|
||||
from mongoengine.queryset import NotUniqueError
|
||||
from apps.recommendations.models import RecommendedFeed
|
||||
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
|
||||
from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds
|
||||
from apps.analyzer.models import apply_classifier_authors, apply_classifier_tags
|
||||
from apps.analyzer.models import get_classifiers_for_user, sort_classifiers_by_feed
|
||||
from apps.profile.models import Profile, MCustomStyling, MDashboardRiver
|
||||
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory, RUserUnreadStory, Feature
|
||||
from apps.reader.forms import SignupForm, LoginForm, FeatureForm
|
||||
from apps.rss_feeds.models import MFeedIcon, MStarredStoryCounts, MSavedSearch
|
||||
from django.views.decorators.cache import never_cache
|
||||
from mongoengine.queryset import NotUniqueError, OperationError
|
||||
|
||||
from apps.analyzer.models import (
|
||||
MClassifierAuthor,
|
||||
MClassifierFeed,
|
||||
MClassifierTag,
|
||||
MClassifierTitle,
|
||||
apply_classifier_authors,
|
||||
apply_classifier_feeds,
|
||||
apply_classifier_tags,
|
||||
apply_classifier_titles,
|
||||
get_classifiers_for_user,
|
||||
sort_classifiers_by_feed,
|
||||
)
|
||||
from apps.notifications.models import MUserFeedNotification
|
||||
from apps.profile.models import MCustomStyling, MDashboardRiver, Profile
|
||||
from apps.reader.forms import FeatureForm, LoginForm, SignupForm
|
||||
from apps.reader.models import (
|
||||
Feature,
|
||||
RUserStory,
|
||||
RUserUnreadStory,
|
||||
UserSubscription,
|
||||
UserSubscriptionFolders,
|
||||
)
|
||||
from apps.recommendations.models import RecommendedFeed
|
||||
from apps.rss_feeds.models import MFeedIcon, MSavedSearch, MStarredStoryCounts
|
||||
from apps.search.models import MUserSearch
|
||||
from apps.statistics.models import MStatistics, MAnalyticsLoader
|
||||
from apps.statistics.models import MAnalyticsLoader, MStatistics
|
||||
from apps.statistics.rstats import RStats
|
||||
|
||||
# from apps.search.models import SearchStarredStory
|
||||
try:
|
||||
from apps.rss_feeds.models import Feed, MFeedPage, DuplicateFeed, MStory, MStarredStory
|
||||
from apps.rss_feeds.models import (
|
||||
DuplicateFeed,
|
||||
Feed,
|
||||
MFeedPage,
|
||||
MStarredStory,
|
||||
MStory,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
from apps.social.models import MSharedStory, MSocialProfile, MSocialServices
|
||||
from apps.social.models import MSocialSubscription, MActivity, MInteraction
|
||||
from apps.categories.models import MCategory
|
||||
from apps.social.views import load_social_page
|
||||
from apps.rss_feeds.tasks import ScheduleImmediateFetches
|
||||
from utils import json_functions as json
|
||||
from utils.user_functions import get_user, ajax_login_required
|
||||
from utils.user_functions import extract_user_agent
|
||||
from utils.feed_functions import relative_timesince
|
||||
from utils.story_functions import format_story_link_date__short
|
||||
from utils.story_functions import format_story_link_date__long
|
||||
from utils.story_functions import strip_tags
|
||||
from utils import log as logging
|
||||
from utils.view_functions import get_argument_or_404, render_to, is_true
|
||||
from utils.view_functions import required_params
|
||||
from utils.ratelimit import ratelimit
|
||||
from vendor.timezones.utilities import localtime_for_timezone
|
||||
import tweepy
|
||||
|
||||
from apps.categories.models import MCategory
|
||||
from apps.rss_feeds.tasks import ScheduleImmediateFetches
|
||||
from apps.social.models import (
|
||||
MActivity,
|
||||
MInteraction,
|
||||
MSharedStory,
|
||||
MSocialProfile,
|
||||
MSocialServices,
|
||||
MSocialSubscription,
|
||||
)
|
||||
from apps.social.views import load_social_page
|
||||
from utils import json_functions as json
|
||||
from utils import log as logging
|
||||
from utils.feed_functions import relative_timesince
|
||||
from utils.ratelimit import ratelimit
|
||||
from utils.story_functions import (
|
||||
format_story_link_date__long,
|
||||
format_story_link_date__short,
|
||||
strip_tags,
|
||||
)
|
||||
from utils.user_functions import ajax_login_required, extract_user_agent, get_user
|
||||
from utils.view_functions import (
|
||||
get_argument_or_404,
|
||||
is_true,
|
||||
render_to,
|
||||
required_params,
|
||||
)
|
||||
from vendor.timezones.utilities import localtime_for_timezone
|
||||
|
||||
BANNED_URLS = [
|
||||
"brentozar.com",
|
||||
]
|
||||
|
@ -75,8 +116,10 @@ ALLOWED_SUBDOMAINS = [
|
|||
'dev',
|
||||
'www',
|
||||
'hwww',
|
||||
'dwww',
|
||||
# 'beta', # Comment to redirect beta -> www, uncomment to allow beta -> staging (+ dns changes)
|
||||
'staging',
|
||||
'hstaging',
|
||||
'discovery',
|
||||
'debug',
|
||||
'debug3',
|
||||
|
@ -679,7 +722,7 @@ def load_single_feed(request, feed_id):
|
|||
|
||||
if page > 400:
|
||||
logging.user(request, "~BR~FK~SBOver page 400 on single feed: %s" % page)
|
||||
assert False
|
||||
raise Http404
|
||||
|
||||
if query:
|
||||
if user.profile.is_premium:
|
||||
|
@ -879,7 +922,6 @@ def load_feed_page(request, feed_id):
|
|||
raise Http404
|
||||
|
||||
feed = Feed.get_by_id(feed_id)
|
||||
|
||||
if feed and feed.has_page and not feed.has_page_exception:
|
||||
if settings.BACKED_BY_AWS.get('pages_on_node'):
|
||||
domain = Site.objects.get_current().domain
|
||||
|
@ -888,8 +930,9 @@ def load_feed_page(request, feed_id):
|
|||
feed.pk,
|
||||
)
|
||||
try:
|
||||
page_response = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
page_response = requests.get(url, verify=not settings.DEBUG)
|
||||
except requests.ConnectionError as e:
|
||||
logging.user(request, f"~FR~SBError loading original page: {url} {e}")
|
||||
page_response = None
|
||||
if page_response and page_response.status_code == 200:
|
||||
response = HttpResponse(page_response.content, content_type="text/html; charset=utf-8")
|
||||
|
|
|
@ -1,33 +1,36 @@
|
|||
import urllib.request
|
||||
import base64
|
||||
import datetime
|
||||
import gzip
|
||||
import http.client
|
||||
import operator
|
||||
import struct
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
from socket import error as SocketError
|
||||
|
||||
import boto3
|
||||
import lxml.html
|
||||
import numpy
|
||||
import scipy
|
||||
import scipy.misc
|
||||
import scipy.cluster
|
||||
import struct
|
||||
import operator
|
||||
import gzip
|
||||
import datetime
|
||||
import requests
|
||||
import base64
|
||||
import http.client
|
||||
from PIL import BmpImagePlugin, PngImagePlugin, Image
|
||||
from socket import error as SocketError
|
||||
import boto3
|
||||
from io import BytesIO
|
||||
import scipy
|
||||
import scipy.cluster
|
||||
import scipy.misc
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.sites.models import Site
|
||||
from apps.rss_feeds.models import MFeedPage, MFeedIcon
|
||||
from utils.facebook_fetcher import FacebookFetcher
|
||||
from utils import log as logging
|
||||
from utils.feed_functions import timelimit, TimeoutError
|
||||
from OpenSSL.SSL import Error as OpenSSLError, SESS_CACHE_NO_INTERNAL_STORE
|
||||
from django.http import HttpResponse
|
||||
from OpenSSL.SSL import SESS_CACHE_NO_INTERNAL_STORE
|
||||
from OpenSSL.SSL import Error as OpenSSLError
|
||||
from PIL import BmpImagePlugin, Image, PngImagePlugin
|
||||
from pyasn1.error import PyAsn1Error
|
||||
from requests.packages.urllib3.exceptions import LocationParseError
|
||||
|
||||
from apps.rss_feeds.models import MFeedIcon, MFeedPage
|
||||
from utils import log as logging
|
||||
from utils.facebook_fetcher import FacebookFetcher
|
||||
from utils.feed_functions import TimeoutError, timelimit
|
||||
|
||||
|
||||
class IconImporter(object):
|
||||
|
||||
|
@ -206,8 +209,10 @@ class IconImporter(object):
|
|||
if self.page_data:
|
||||
content = self.page_data
|
||||
elif settings.BACKED_BY_AWS.get('pages_on_node'):
|
||||
domain = Site.objects.get_current().domain
|
||||
url = "https://%s/original_page/%s" % (
|
||||
domain = "node-page.service.consul:8008"
|
||||
if settings.DOCKERBUILD:
|
||||
domain = "node:8008"
|
||||
url = "http://%s/original_page/%s" % (
|
||||
domain,
|
||||
self.feed.pk,
|
||||
)
|
||||
|
|
|
@ -462,6 +462,10 @@ class Feed(models.Model):
|
|||
username = re.search('youtube.com/user/(\w+)', url).group(1)
|
||||
url = "http://gdata.youtube.com/feeds/base/users/%s/uploads" % username
|
||||
without_rss = True
|
||||
if url and 'youtube.com/@' in url:
|
||||
username = url.split('youtube.com/@')[1]
|
||||
url = "http://gdata.youtube.com/feeds/base/users/%s/uploads" % username
|
||||
without_rss = True
|
||||
if url and 'youtube.com/channel/' in url:
|
||||
channel_id = re.search('youtube.com/channel/([-_\w]+)', url).group(1)
|
||||
url = "https://www.youtube.com/feeds/videos.xml?channel_id=%s" % channel_id
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
import requests
|
||||
import re
|
||||
import traceback
|
||||
import feedparser
|
||||
import time
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
import http.client
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import zlib
|
||||
from socket import error as SocketError
|
||||
|
||||
import feedparser
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.utils.encoding import smart_bytes
|
||||
from mongoengine.queryset import NotUniqueError
|
||||
from socket import error as SocketError
|
||||
from django.conf import settings
|
||||
from django.utils.text import compress_string as compress_string_with_gzip
|
||||
from utils import log as logging
|
||||
from apps.rss_feeds.models import MFeedPage
|
||||
from utils.feed_functions import timelimit, TimeoutError
|
||||
from mongoengine.queryset import NotUniqueError
|
||||
from OpenSSL.SSL import Error as OpenSSLError
|
||||
from pyasn1.error import PyAsn1Error
|
||||
from sentry_sdk import capture_exception, flush
|
||||
|
||||
from apps.rss_feeds.models import MFeedPage
|
||||
from utils import log as logging
|
||||
from utils.feed_functions import TimeoutError, timelimit
|
||||
|
||||
# from utils.feed_functions import mail_feed_error_to_admin
|
||||
|
||||
BROKEN_PAGES = [
|
||||
|
@ -127,6 +132,7 @@ class PageImporter(object):
|
|||
return
|
||||
except (ValueError, urllib.error.URLError, http.client.BadStatusLine, http.client.InvalidURL,
|
||||
requests.exceptions.ConnectionError) as e:
|
||||
logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e))
|
||||
self.feed.save_page_history(401, "Bad URL", e)
|
||||
try:
|
||||
fp = feedparser.parse(self.feed.feed_address)
|
||||
|
@ -134,10 +140,8 @@ class PageImporter(object):
|
|||
return html
|
||||
feed_link = fp.feed.get('link', "")
|
||||
self.feed.save()
|
||||
logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e))
|
||||
except (urllib.error.HTTPError) as e:
|
||||
self.feed.save_page_history(e.code, e.msg, e.fp.read())
|
||||
except (http.client.IncompleteRead) as e:
|
||||
logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e))
|
||||
self.feed.save_page_history(500, "IncompleteRead", e)
|
||||
except (requests.exceptions.RequestException,
|
||||
requests.packages.urllib3.exceptions.HTTPError) as e:
|
||||
|
@ -305,8 +309,10 @@ class PageImporter(object):
|
|||
return feed_page
|
||||
|
||||
def save_page_node(self, html):
|
||||
domain = Site.objects.get_current().domain
|
||||
url = "https://%s/original_page/%s" % (
|
||||
domain = "node-page.service.consul:8008"
|
||||
if settings.DOCKERBUILD:
|
||||
domain = "node:8008"
|
||||
url = "http://%s/original_page/%s" % (
|
||||
domain,
|
||||
self.feed.pk,
|
||||
)
|
||||
|
|
|
@ -2,13 +2,16 @@ import datetime
|
|||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import redis
|
||||
from newsblur_web.celeryapp import app
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
from utils import log as logging
|
||||
from django.conf import settings
|
||||
|
||||
from apps.profile.middleware import DBProfilerMiddleware
|
||||
from newsblur_web.celeryapp import app
|
||||
from utils import log as logging
|
||||
from utils.redis_raw_log_middleware import RedisDumpMiddleware
|
||||
|
||||
FEED_TASKING_MAX = 10000
|
||||
|
||||
@app.task(name='task-feeds')
|
||||
|
@ -45,6 +48,7 @@ def TaskFeeds():
|
|||
else:
|
||||
logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
|
||||
active_count = 0
|
||||
feeds = []
|
||||
|
||||
logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
|
||||
active_count,
|
||||
|
|
75
archive/hetzner migration.md
Normal file
75
archive/hetzner migration.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
Migration from Digital Ocean to Hetzner, covering about 120 servers (20 db, 80 task, 20 app)
|
||||
|
||||
## Tweet
|
||||
|
||||
> "Time for a big server transition. I’m moving the servers from Digital Ocean to Hetzner. Every database server (postgresql, mongo, redis x 4, elasticsearch, prometheus, consul, and sentry) is making the move. I’m going to do it all at once, which means about an hour of downtime."
|
||||
|
||||
> "Afterwards, you shouldn't notice anything different, although these are bare metal servers, so theoretically they should be faster and more reliable."
|
||||
|
||||
```
|
||||
make maintenance_on
|
||||
make celery_stop
|
||||
```
|
||||
|
||||
## Postgres
|
||||
|
||||
> Edit postgres/consul_service.json: db-postgres2 -> hdb-postgres-1
|
||||
|
||||
```
|
||||
aps -l db-postgres2,hdb-postgres-1,hdb-postgres-2 -t consul
|
||||
aps -l hdb-postgres-1 -t pg_promote
|
||||
```
|
||||
|
||||
## Mongo
|
||||
|
||||
```
|
||||
sshdo db-mongo-primary1
|
||||
sudo docker exec -it mongo mongo
|
||||
rs.config()
|
||||
rs.reconfig()
|
||||
```
|
||||
|
||||
## Mongo analytics
|
||||
|
||||
> Edit mongo/tasks/main.yml: mongo_analytics_secondary
|
||||
|
||||
```
|
||||
aps -l db-mongo-analytics2,hdb-mongo-analytics-1 -t consul
|
||||
```
|
||||
|
||||
## Redis
|
||||
|
||||
> Edit redis/tasks/main.yml: redis_secondary
|
||||
|
||||
```
|
||||
aps -l hdb-redis-user-1,hdb-redis-user-2,db-redis-user -t consul
|
||||
aps -l hdb-redis-session-1,hdb-redis-session-2,db-redis-sessions -t consul
|
||||
aps -l hdb-redis-story-1,hdb-redis-story-2,db-redis-story1 -t consul
|
||||
aps -l hdb-redis-pubsub,db-redis-pubsub -t consul
|
||||
apd -l hdb-redis-user-1,hdb-redis-session-1,hdb-redis-story-1,hdb-redis-pubsub -t replicaofnoone
|
||||
```
|
||||
|
||||
## Elasticsearch
|
||||
|
||||
> Edit elasticsearch/tasks/main.yml: elasticsearch_secondary
|
||||
```
|
||||
aps -l db-elasticsearch1,hdb-elasticsearch-1 -t consul
|
||||
```
|
||||
> Eventually `MUserSearch.remove_all()`
|
||||
|
||||
## Test hwww.newsblur.com
|
||||
```
|
||||
ansible-playbook ansible/deploy.yml -l happ-web-01 --tags maintenance_off
|
||||
```
|
||||
|
||||
## Looks good? Launch
|
||||
|
||||
> Haproxy on DO to redirect to Hetzner
|
||||
```
|
||||
aps -l www -t haproxy
|
||||
```
|
||||
> Change DNS to point to Hetzner
|
||||
```
|
||||
make maintenance_off
|
||||
```
|
||||
|
|
@ -44,6 +44,6 @@ dependencies {
|
|||
|
||||
androidComponents {
|
||||
beforeVariants(selector().all()) {
|
||||
it.enabled = it.buildType == Const.benchmark
|
||||
it.enable = it.buildType == Const.benchmark
|
||||
}
|
||||
}
|
|
@ -46,11 +46,11 @@ android {
|
|||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(Dependencies.coreKtx)
|
||||
implementation(Dependencies.fragment)
|
||||
implementation(Dependencies.recyclerView)
|
||||
implementation(Dependencies.swipeRefreshLayout)
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Prevent R8 from leaving Data object members always null
|
||||
-keepclassmembers, allowobfuscation class * {
|
||||
# R8
|
||||
-if class *
|
||||
-keepclasseswithmembers, allowobfuscation class <1> {
|
||||
<init>(...);
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep, allowobfuscation, allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep, allowobfuscation, allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
package com.newsblur
|
||||
|
||||
import android.database.AbstractWindowedCursor
|
||||
import android.database.Cursor
|
||||
import android.database.CursorWindow
|
||||
import android.database.sqlite.SQLiteBlobTooBigException
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteDatabase.OpenParams
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
|
@ -12,8 +18,11 @@ import com.newsblur.serialization.ClassifierMapTypeAdapter
|
|||
import com.newsblur.serialization.DateStringTypeAdapter
|
||||
import com.newsblur.serialization.StoriesResponseTypeAdapter
|
||||
import com.newsblur.serialization.StoryTypeAdapter
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.lang.reflect.Field
|
||||
import java.util.Arrays
|
||||
import java.util.Date
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
@ -32,4 +41,40 @@ class ParsingTest {
|
|||
|
||||
val input = """""".trimIndent()
|
||||
}
|
||||
|
||||
/**
|
||||
* https://android.googlesource.com/platform/cts/+/master/tests/tests/database/src/android/database/sqlite/cts/SQLiteCursorTest.java
|
||||
* New cursor window constructor from API 28+
|
||||
*/
|
||||
@Test
|
||||
fun testRowTooBig() {
|
||||
val mDatabase = SQLiteDatabase.createInMemory(OpenParams.Builder().build())
|
||||
mDatabase.execSQL("CREATE TABLE Tst (Txt BLOB NOT NULL);")
|
||||
val testArr = ByteArray(10000)
|
||||
Arrays.fill(testArr, 1.toByte())
|
||||
for (i in 0..9) {
|
||||
mDatabase.execSQL("INSERT INTO Tst VALUES (?)", arrayOf<Any>(testArr))
|
||||
}
|
||||
// Now reduce window size, so that no rows can fit
|
||||
val cursor: Cursor = mDatabase.rawQuery("SELECT * FROM TST", null)
|
||||
val cw = CursorWindow("test", 5000)
|
||||
val ac = cursor as AbstractWindowedCursor
|
||||
ac.window = cw
|
||||
try {
|
||||
ac.moveToNext()
|
||||
fail("Exception is expected when row exceeds CursorWindow size")
|
||||
} catch (expected: SQLiteBlobTooBigException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cursorWindowReflection() {
|
||||
try {
|
||||
val field: Field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize")
|
||||
field.isAccessible = true
|
||||
field.set(null, 100 * 1024 * 1024) //the 100MB is the new size
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -155,6 +155,13 @@
|
|||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".util.DownloadCompleteReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" />
|
||||
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
|
||||
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.os.Bundle;
|
|||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
@ -27,11 +26,7 @@ public class AddFeedExternal extends NbActivity implements AddFeedFragment.AddFe
|
|||
|
||||
UIUtils.setupToolbar(this, R.drawable.logo, "Add Feed", true);
|
||||
|
||||
binding.loadingThrob.setEnabled(!ViewUtils.isPowerSaveMode(this));
|
||||
binding.loadingThrob.setColors(ContextCompat.getColor(this, R.color.refresh_1),
|
||||
ContextCompat.getColor(this, R.color.refresh_2),
|
||||
ContextCompat.getColor(this, R.color.refresh_3),
|
||||
ContextCompat.getColor(this, R.color.refresh_4));
|
||||
binding.loadingIndicator.setEnabled(!ViewUtils.isPowerSaveMode(this));
|
||||
|
||||
Intent intent = getIntent();
|
||||
Uri uri = intent.getData();
|
||||
|
@ -53,7 +48,7 @@ public class AddFeedExternal extends NbActivity implements AddFeedFragment.AddFe
|
|||
public void run() {
|
||||
binding.progressText.setText(R.string.adding_feed_progress);
|
||||
binding.progressText.setVisibility(View.VISIBLE);
|
||||
binding.loadingThrob.setVisibility(View.VISIBLE);
|
||||
binding.loadingIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,32 +5,43 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.newsblur.R
|
||||
import com.newsblur.databinding.ActivityImportExportBinding
|
||||
import com.newsblur.network.APIConstants
|
||||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.service.NBSyncService
|
||||
import com.newsblur.util.*
|
||||
import com.newsblur.util.DownloadCompleteReceiver
|
||||
import com.newsblur.util.FeedUtils
|
||||
import com.newsblur.util.FileDownloader
|
||||
import com.newsblur.util.NBScope
|
||||
import com.newsblur.util.UIUtils
|
||||
import com.newsblur.util.executeAsyncTask
|
||||
import com.newsblur.util.setViewGone
|
||||
import com.newsblur.util.setViewVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ImportExportActivity : NbActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var apiManager: APIManager
|
||||
|
||||
private val pickXmlFileRequestCode = 10
|
||||
|
||||
private lateinit var binding: ActivityImportExportBinding
|
||||
|
||||
override fun onCreate(bundle: Bundle?) {
|
||||
super.onCreate(bundle)
|
||||
private val filePickResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
handleFilePickResult(result.data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityImportExportBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
@ -53,13 +64,14 @@ class ImportExportActivity : NbActivity() {
|
|||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = mineType
|
||||
}.also {
|
||||
startActivityForResult(it, pickXmlFileRequestCode)
|
||||
filePickResultLauncher.launch(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportOpmlFile() {
|
||||
val exportOpmlUrl = APIConstants.buildUrl(APIConstants.PATH_EXPORT_OPML)
|
||||
UIUtils.handleUri(this, Uri.parse(exportOpmlUrl))
|
||||
DownloadCompleteReceiver.expectedFileDownloadId = FileDownloader.exportOpml(this)
|
||||
val msg = "${getString(R.string.newsblur_opml)} download started"
|
||||
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun importOpmlFile(uri: Uri) {
|
||||
|
@ -106,18 +118,14 @@ class ImportExportActivity : NbActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
if (requestCode == pickXmlFileRequestCode && resultCode == Activity.RESULT_OK) {
|
||||
resultData?.data?.also { uri ->
|
||||
importOpmlFile(uri)
|
||||
}
|
||||
?: Snackbar.make(
|
||||
binding.root,
|
||||
"OPML file retrieval failed!",
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
private fun handleFilePickResult(resultData: Intent?) {
|
||||
resultData?.data?.also { uri ->
|
||||
importOpmlFile(uri)
|
||||
} ?: Snackbar.make(
|
||||
binding.root,
|
||||
"OPML file retrieval failed!",
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun handleUpdate(updateType: Int) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_REBUILD;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_STORY;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
|
|
@ -70,7 +70,7 @@ class LoginProgress : FragmentActivity() {
|
|||
val startMain = Intent(this, Main::class.java)
|
||||
startActivity(startMain)
|
||||
} else {
|
||||
UIUtils.safeToast(this, it.getErrorMessage(getString(R.string.login_message_error)), Toast.LENGTH_LONG)
|
||||
Toast.makeText(this, it.getErrorMessage(getString(R.string.login_message_error)), Toast.LENGTH_LONG).show()
|
||||
startActivity(Intent(this, Login::class.java))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_DB_READY;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_METADATA;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_DB_READY;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_METADATA;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_REBUILD;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
package com.newsblur.activity
|
||||
|
||||
import android.content.IntentFilter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.newsblur.util.PrefConstants.ThemeValue
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.newsblur.database.BlurDatabaseHelper
|
||||
import com.newsblur.util.PrefsUtils
|
||||
import com.newsblur.util.UIUtils
|
||||
import com.newsblur.service.NBSyncReceiver
|
||||
import com.newsblur.service.NBSync
|
||||
import com.newsblur.service.NbSyncManager
|
||||
import com.newsblur.util.FeedUtils
|
||||
import com.newsblur.util.Log
|
||||
import com.newsblur.util.PrefConstants.ThemeValue
|
||||
import com.newsblur.util.PrefsUtils
|
||||
import com.newsblur.util.UIUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -27,14 +34,7 @@ open class NbActivity : AppCompatActivity() {
|
|||
private var uniqueLoginKey: String? = null
|
||||
private var lastTheme: ThemeValue? = null
|
||||
|
||||
// Facilitates the db updates by the sync service on the UI
|
||||
private val serviceSyncReceiver = object : NBSyncReceiver() {
|
||||
override fun handleUpdateType(updateType: Int) {
|
||||
runOnUiThread { handleUpdate(updateType) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(bundle: Bundle?) {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Log.offerContext(this)
|
||||
Log.d(this, "onCreate")
|
||||
|
||||
|
@ -43,7 +43,7 @@ open class NbActivity : AppCompatActivity() {
|
|||
PrefsUtils.applyThemePreference(this)
|
||||
lastTheme = PrefsUtils.getSelectedTheme(this)
|
||||
|
||||
super.onCreate(bundle)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// in rare cases of process interruption or DB corruption, an activity can launch without valid
|
||||
// login creds. redirect the user back to the loging workflow.
|
||||
|
@ -53,7 +53,7 @@ open class NbActivity : AppCompatActivity() {
|
|||
finish()
|
||||
}
|
||||
|
||||
bundle?.let {
|
||||
savedInstanceState?.let {
|
||||
uniqueLoginKey = it.getString(UNIQUE_LOGIN_KEY)
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,19 @@ open class NbActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
finishIfNotLoggedIn()
|
||||
|
||||
// Facilitates the db updates by the sync service on the UI
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
launch {
|
||||
NbSyncManager.state.collectLatest {
|
||||
withContext(Dispatchers.Main) {
|
||||
handleSyncUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -76,16 +89,11 @@ open class NbActivity : AppCompatActivity() {
|
|||
PrefsUtils.applyThemePreference(this)
|
||||
UIUtils.restartActivity(this)
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(this, serviceSyncReceiver, IntentFilter().apply {
|
||||
addAction(NBSyncReceiver.NB_SYNC_ACTION)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Log.d(this.javaClass.name, "onPause")
|
||||
super.onPause()
|
||||
unregisterReceiver(serviceSyncReceiver)
|
||||
}
|
||||
|
||||
private fun finishIfNotLoggedIn() {
|
||||
|
@ -109,15 +117,18 @@ open class NbActivity : AppCompatActivity() {
|
|||
FeedUtils.triggerSync(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on each NB activity after the DB has been updated by the sync service.
|
||||
*
|
||||
* @param updateType one or more of the UPDATE_* flags in this class to indicate the
|
||||
* type of update being broadcast.
|
||||
*/
|
||||
private fun handleSyncUpdate(nbSync: NBSync) = when (nbSync) {
|
||||
is NBSync.Update -> handleUpdate(nbSync.type)
|
||||
is NBSync.Error -> handleErrorMsg(nbSync.msg)
|
||||
}
|
||||
|
||||
protected open fun handleUpdate(updateType: Int) {
|
||||
Log.w(this, "activity doesn't implement handleUpdate")
|
||||
}
|
||||
|
||||
private fun handleErrorMsg(msg: String) {
|
||||
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private const val UNIQUE_LOGIN_KEY = "uniqueLoginKey"
|
|
@ -27,8 +27,8 @@ class NotificationsActivity : NbActivity(), NotificationsAdapter.Listener {
|
|||
private lateinit var viewModel: NotificationsViewModel
|
||||
private lateinit var adapter: NotificationsAdapter
|
||||
|
||||
override fun onCreate(bundle: Bundle?) {
|
||||
super.onCreate(bundle)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = ViewModelProvider(this)[NotificationsViewModel::class.java]
|
||||
binding = ActivityNotificationsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
|
|
@ -60,9 +60,9 @@ class Profile : NbActivity() {
|
|||
loadUserDetails()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
userId?.let { outState.putString(USER_ID, it) }
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(savedInstanceState)
|
||||
userId?.let { savedInstanceState.putString(USER_ID, it) }
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
|
|
@ -30,9 +30,9 @@ import com.newsblur.fragment.ReadingPagerFragment
|
|||
import com.newsblur.keyboard.KeyboardEvent
|
||||
import com.newsblur.keyboard.KeyboardListener
|
||||
import com.newsblur.keyboard.KeyboardManager
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_REBUILD
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STATUS
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_REBUILD
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_STATUS
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_STORY
|
||||
import com.newsblur.service.NBSyncService
|
||||
import com.newsblur.util.AppConstants
|
||||
import com.newsblur.util.CursorFilters
|
||||
|
@ -116,7 +116,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
|
|||
|
||||
override fun onCreate(savedInstanceBundle: Bundle?) {
|
||||
super.onCreate(savedInstanceBundle)
|
||||
storiesViewModel = ViewModelProvider(this).get(StoriesViewModel::class.java)
|
||||
storiesViewModel = ViewModelProvider(this)[StoriesViewModel::class.java]
|
||||
binding = ActivityReadingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
@ -159,20 +159,20 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
|
|||
getActiveStoriesCursor(this, true)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(savedInstanceState)
|
||||
if (storyHash != null) {
|
||||
outState.putString(EXTRA_STORY_HASH, storyHash)
|
||||
savedInstanceState.putString(EXTRA_STORY_HASH, storyHash)
|
||||
} else if (pager != null) {
|
||||
val currentItem = pager!!.currentItem
|
||||
val story = readingAdapter!!.getStory(currentItem)
|
||||
if (story != null) {
|
||||
outState.putString(EXTRA_STORY_HASH, story.storyHash)
|
||||
savedInstanceState.putString(EXTRA_STORY_HASH, story.storyHash)
|
||||
}
|
||||
}
|
||||
|
||||
if (startingUnreadCount != 0) {
|
||||
outState.putInt(BUNDLE_STARTING_UNREAD, startingUnreadCount)
|
||||
savedInstanceState.putInt(BUNDLE_STARTING_UNREAD, startingUnreadCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,8 +60,8 @@ class SubscriptionActivity : NbActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreate(bundle: Bundle?) {
|
||||
super.onCreate(bundle)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySubscriptionBinding.inflate(layoutInflater)
|
||||
bindingPremium = ViewPremiumSubscriptionBinding.bind(binding.containerPremiumSubscription.root)
|
||||
bindingArchive = ViewArchiveSubscriptionBinding.bind(binding.containerArchiveSubscription.root)
|
||||
|
|
|
@ -15,15 +15,13 @@ import com.newsblur.domain.Story
|
|||
import com.newsblur.fragment.LoadingFragment
|
||||
import com.newsblur.fragment.ReadingItemFragment
|
||||
import com.newsblur.fragment.ReadingItemFragment.Companion.newInstance
|
||||
import com.newsblur.service.NBSyncReceiver
|
||||
import com.newsblur.service.NbSyncManager
|
||||
import com.newsblur.util.Log
|
||||
import com.newsblur.util.NBScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* An adapter to display stories in a ViewPager. Loosely based upon FragmentStatePagerAdapter, but
|
||||
|
@ -228,7 +226,7 @@ class ReadingAdapter(
|
|||
for (s in stories) {
|
||||
fragments[s.storyHash]?.let { rif ->
|
||||
rif.offerStoryUpdate(s)
|
||||
rif.handleUpdate(NBSyncReceiver.UPDATE_STORY)
|
||||
rif.handleUpdate(NbSyncManager.UPDATE_STORY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,9 +24,12 @@ import android.widget.RelativeLayout;
|
|||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.activity.FeedItemsList;
|
||||
|
@ -43,6 +46,8 @@ import com.newsblur.util.PrefsUtils;
|
|||
import com.newsblur.util.SpacingStyle;
|
||||
import com.newsblur.util.StoryContentPreviewStyle;
|
||||
import com.newsblur.util.StoryListStyle;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.util.StoryUtil;
|
||||
import com.newsblur.util.StoryUtils;
|
||||
import com.newsblur.util.ThumbnailStyle;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
@ -90,6 +95,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
private final UserDetails user;
|
||||
private ThumbnailStyle thumbnailStyle;
|
||||
private SpacingStyle spacingStyle;
|
||||
private StoryOrder storyOrder;
|
||||
|
||||
public StoryViewAdapter(NbActivity context,
|
||||
ItemSetFragment fragment,
|
||||
|
@ -123,6 +129,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
user = PrefsUtils.getUserDetails(context);
|
||||
thumbnailStyle = PrefsUtils.getThumbnailStyle(context);
|
||||
spacingStyle = PrefsUtils.getSpacingStyle(context);
|
||||
storyOrder = PrefsUtils.getStoryOrder(context, fs);
|
||||
|
||||
executorService = Executors.newFixedThreadPool(1);
|
||||
|
||||
|
@ -195,12 +202,12 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
if (position >= getStoryCount()) {
|
||||
return (footerViews.get(position - getStoryCount()).hashCode());
|
||||
}
|
||||
|
||||
|
||||
if (position >= stories.size() || position < 0) return 0;
|
||||
return stories.get(position).storyHash.hashCode();
|
||||
}
|
||||
|
||||
public void swapCursor(final Cursor c, final RecyclerView rv, Parcelable oldScrollState) {
|
||||
public void swapCursor(final Cursor c, final RecyclerView rv, Parcelable oldScrollState, final boolean skipBackFillingStories) {
|
||||
// cache the identity of the most recent cursor so async batches can check to
|
||||
// see if they are stale
|
||||
cursor = c;
|
||||
|
@ -213,7 +220,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
thawDiffUpdate(c, rv);
|
||||
thawDiffUpdate(c, rv, skipBackFillingStories);
|
||||
}
|
||||
};
|
||||
executorService.submit(r);
|
||||
|
@ -223,7 +230,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
* Attempt to thaw a new set of stories from the cursor most recently
|
||||
* seen when the that cycle started.
|
||||
*/
|
||||
private void thawDiffUpdate(final Cursor c, final RecyclerView rv) {
|
||||
private void thawDiffUpdate(final Cursor c, final RecyclerView rv, final boolean skipBackFillingStories) {
|
||||
if (c != cursor) return;
|
||||
|
||||
// thawed stories
|
||||
|
@ -234,14 +241,42 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
// as a new one will be provided and another cycle will start. just return.
|
||||
try {
|
||||
if (c == null) {
|
||||
newStories = new ArrayList<Story>(0);
|
||||
newStories = new ArrayList<>();
|
||||
} else {
|
||||
if (c.isClosed()) return;
|
||||
newStories = new ArrayList<Story>(c.getCount());
|
||||
newStories = new ArrayList<>(c.getCount());
|
||||
c.moveToPosition(-1);
|
||||
|
||||
// The 'skipBackFillingStories' flag is used to ensure that when the adapter resumes,
|
||||
// it omits any new stories that would disrupt the current order and cause the list to
|
||||
// unexpectedly jump, thereby preserving the scroll position. This flag specifically helps
|
||||
// manage the insertion of new stories that have been backfilled according to their timestamps.
|
||||
Set<String> currentStoryHashes = skipBackFillingStories ?
|
||||
StoryUtil.getStoryHashes(stories) : Collections.emptySet();
|
||||
Long storyTimestampThreshold;
|
||||
if (skipBackFillingStories && storyOrder == StoryOrder.NEWEST) {
|
||||
storyTimestampThreshold = StoryUtil.getOldestStoryTimestamp(stories);
|
||||
} else if (skipBackFillingStories && storyOrder == StoryOrder.OLDEST) {
|
||||
storyTimestampThreshold = StoryUtil.getNewestStoryTimestamp(stories);
|
||||
} else {
|
||||
storyTimestampThreshold = null;
|
||||
}
|
||||
|
||||
while (c.moveToNext()) {
|
||||
if (c.isClosed()) return;
|
||||
Story s = Story.fromCursor(c);
|
||||
if (skipBackFillingStories && !currentStoryHashes.contains(s.storyHash)) {
|
||||
if (storyOrder == StoryOrder.NEWEST &&
|
||||
storyTimestampThreshold != null &&
|
||||
s.timestamp >= storyTimestampThreshold) {
|
||||
continue;
|
||||
} else if (storyOrder == StoryOrder.OLDEST &&
|
||||
storyTimestampThreshold != null &&
|
||||
s.timestamp <= storyTimestampThreshold) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
s.bindExternValues(c);
|
||||
newStories.add(s);
|
||||
if (! s.read) indexOfLastUnread = c.getPosition();
|
||||
|
@ -336,8 +371,8 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
}
|
||||
|
||||
public class StoryViewHolder extends RecyclerView.ViewHolder
|
||||
implements View.OnClickListener,
|
||||
View.OnCreateContextMenuListener,
|
||||
implements View.OnClickListener,
|
||||
View.OnCreateContextMenuListener,
|
||||
MenuItem.OnMenuItemClickListener,
|
||||
View.OnTouchListener {
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.text.TextUtils;
|
|||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.util.StoryUtil;
|
||||
|
||||
public class Comment implements Serializable {
|
||||
|
||||
|
@ -55,7 +56,7 @@ public class Comment implements Serializable {
|
|||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.COMMENT_DATE, date);
|
||||
values.put(DatabaseConstants.COMMENT_STORYID, storyId);
|
||||
values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", likingUsers));
|
||||
values.put(DatabaseConstants.COMMENT_LIKING_USERS, StoryUtil.nullSafeJoin(",", likingUsers));
|
||||
values.put(DatabaseConstants.COMMENT_TEXT, commentText);
|
||||
values.put(DatabaseConstants.COMMENT_SHAREDDATE, sharedDate);
|
||||
values.put(DatabaseConstants.COMMENT_BYFRIEND, byFriend ? "true" : "false");
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.google.gson.annotations.SerializedName;
|
|||
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.StoryUtil;
|
||||
|
||||
public class Story implements Serializable {
|
||||
|
||||
|
@ -139,21 +140,21 @@ public class Story implements Serializable {
|
|||
values.put(DatabaseConstants.STORY_AUTHORS, authors);
|
||||
values.put(DatabaseConstants.STORY_SOCIAL_USER_ID, socialUserId);
|
||||
values.put(DatabaseConstants.STORY_SOURCE_USER_ID, sourceUserId);
|
||||
values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", sharedUserIds));
|
||||
values.put(DatabaseConstants.STORY_FRIEND_USER_IDS, TextUtils.join(",", friendUserIds));
|
||||
values.put(DatabaseConstants.STORY_SHARED_USER_IDS, StoryUtil.nullSafeJoin(",", sharedUserIds));
|
||||
values.put(DatabaseConstants.STORY_FRIEND_USER_IDS, StoryUtil.nullSafeJoin(",", friendUserIds));
|
||||
values.put(DatabaseConstants.STORY_INTELLIGENCE_AUTHORS, intelligence.intelligenceAuthors);
|
||||
values.put(DatabaseConstants.STORY_INTELLIGENCE_FEED, intelligence.intelligenceFeed);
|
||||
values.put(DatabaseConstants.STORY_INTELLIGENCE_TAGS, intelligence.intelligenceTags);
|
||||
values.put(DatabaseConstants.STORY_INTELLIGENCE_TITLE, intelligence.intelligenceTitle);
|
||||
values.put(DatabaseConstants.STORY_INTELLIGENCE_TOTAL, intelligence.calcTotalIntel());
|
||||
values.put(DatabaseConstants.STORY_TAGS, TextUtils.join(",", tags));
|
||||
values.put(DatabaseConstants.STORY_USER_TAGS, TextUtils.join(",", userTags));
|
||||
values.put(DatabaseConstants.STORY_TAGS, StoryUtil.nullSafeJoin(",", tags));
|
||||
values.put(DatabaseConstants.STORY_USER_TAGS, StoryUtil.nullSafeJoin(",", userTags));
|
||||
values.put(DatabaseConstants.STORY_READ, read);
|
||||
values.put(DatabaseConstants.STORY_STARRED, starred);
|
||||
values.put(DatabaseConstants.STORY_STARRED_DATE, starredTimestamp);
|
||||
values.put(DatabaseConstants.STORY_FEED_ID, feedId);
|
||||
values.put(DatabaseConstants.STORY_HASH, storyHash);
|
||||
values.put(DatabaseConstants.STORY_IMAGE_URLS, TextUtils.join(",", imageUrls));
|
||||
values.put(DatabaseConstants.STORY_IMAGE_URLS, StoryUtil.nullSafeJoin(",", imageUrls));
|
||||
values.put(DatabaseConstants.STORY_LAST_READ_DATE, lastReadTimestamp);
|
||||
values.put(DatabaseConstants.STORY_SHARED_DATE, sharedTimestamp);
|
||||
values.put(DatabaseConstants.STORY_SEARCH_HIT, searchHit);
|
||||
|
@ -299,11 +300,11 @@ public class Story implements Serializable {
|
|||
return true;
|
||||
}
|
||||
|
||||
private static final Pattern ytSniff1 = Pattern.compile("youtube\\.com\\/embed\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ytSniff2 = Pattern.compile("youtube\\.com\\/v\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ytSniff3 = Pattern.compile("ytimg\\.com\\/vi\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ytSniff4 = Pattern.compile("youtube\\.com\\/watch\\?v=([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final String YT_THUMB_PRE = "http://img.youtube.com/vi/";
|
||||
private static final Pattern ytSniff1 = Pattern.compile("youtube\\.com/embed/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ytSniff2 = Pattern.compile("youtube\\.com/v/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ytSniff3 = Pattern.compile("ytimg\\.com/vi/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ytSniff4 = Pattern.compile("youtube\\.com/watch\\?v=([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final String YT_THUMB_PRE = "https://img.youtube.com/vi/";
|
||||
private static final String YT_THUMB_POST = "/0.jpg";
|
||||
|
||||
public static String guessStoryThumbnailURL(Story story) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
@ -13,7 +12,6 @@ import android.os.Bundle;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -67,12 +65,10 @@ public class ChooseFoldersFragment extends DialogFragment {
|
|||
}
|
||||
}
|
||||
|
||||
final Activity activity = getActivity();
|
||||
LayoutInflater inflater = LayoutInflater.from(activity);
|
||||
View v = inflater.inflate(R.layout.dialog_choosefolders, null);
|
||||
View v = getLayoutInflater().inflate(R.layout.dialog_choosefolders, null);
|
||||
DialogChoosefoldersBinding binding = DialogChoosefoldersBinding.bind(v);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
builder.setTitle(String.format(getResources().getString(R.string.title_choose_folders), feed.title));
|
||||
builder.setView(v);
|
||||
|
||||
|
@ -85,16 +81,16 @@ public class ChooseFoldersFragment extends DialogFragment {
|
|||
builder.setPositiveButton(R.string.dialog_folders_save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
feedUtils.moveFeedToFolders(activity, feed.feedId, newFolders, oldFolders);
|
||||
feedUtils.moveFeedToFolders(requireContext(), feed.feedId, newFolders, oldFolders);
|
||||
ChooseFoldersFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
ListAdapter adapter = new ArrayAdapter<Folder>(getActivity(), R.layout.row_choosefolders, R.id.choosefolders_foldername, folders) {
|
||||
ListAdapter adapter = new ArrayAdapter<>(requireContext(), R.layout.row_choosefolders, R.id.choosefolders_foldername, folders) {
|
||||
@Override
|
||||
public View getView(final int position, View convertView, ViewGroup parent) {
|
||||
View v = super.getView(position, convertView, parent);
|
||||
CheckBox row = (CheckBox) v.findViewById(R.id.choosefolders_foldername);
|
||||
CheckBox row = v.findViewById(R.id.choosefolders_foldername);
|
||||
if (position == 0) {
|
||||
row.setText(R.string.top_level);
|
||||
}
|
||||
|
|
|
@ -82,11 +82,11 @@ public class DeleteFeedFragment extends DialogFragment {
|
|||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
if (getArguments().getString(FEED_TYPE).equals(NORMAL_FEED)) {
|
||||
feedUtils.deleteFeed(getArguments().getString(FEED_ID), getArguments().getString(FOLDER_NAME), getActivity());
|
||||
feedUtils.deleteFeed(getArguments().getString(FEED_ID), getArguments().getString(FOLDER_NAME));
|
||||
} else if (getArguments().getString(FEED_TYPE).equals(SAVED_SEARCH_FEED)) {
|
||||
feedUtils.deleteSavedSearch(getArguments().getString(FEED_ID), getArguments().getString(QUERY), getActivity());
|
||||
feedUtils.deleteSavedSearch(getArguments().getString(FEED_ID), getArguments().getString(QUERY));
|
||||
} else {
|
||||
feedUtils.deleteSocialFeed(getArguments().getString(FEED_ID), getActivity());
|
||||
feedUtils.deleteSocialFeed(getArguments().getString(FEED_ID));
|
||||
}
|
||||
// if called from a feed view, end it
|
||||
Activity activity = DeleteFeedFragment.this.getActivity();
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package com.newsblur.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
|
@ -43,33 +41,31 @@ public class EditReplyDialogFragment extends DialogFragment {
|
|||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
final Story story = (Story) getArguments().getSerializable(STORY);
|
||||
final String commentUserId = getArguments().getString(COMMENT_USER_ID);
|
||||
final String replyId = getArguments().getString(REPLY_ID);
|
||||
String replyText = getArguments().getString(REPLY_TEXT);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
builder.setTitle(R.string.edit_reply);
|
||||
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(activity);
|
||||
View replyView = layoutInflater.inflate(R.layout.reply_dialog, null);
|
||||
View replyView = getLayoutInflater().inflate(R.layout.reply_dialog, null);
|
||||
builder.setView(replyView);
|
||||
final EditText reply = (EditText) replyView.findViewById(R.id.reply_field);
|
||||
final EditText reply = replyView.findViewById(R.id.reply_field);
|
||||
reply.setText(replyText);
|
||||
|
||||
builder.setPositiveButton(R.string.edit_reply_update, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
String replyText = reply.getText().toString();
|
||||
feedUtils.updateReply(activity, story, commentUserId, replyId, replyText);
|
||||
feedUtils.updateReply(requireContext(), story, commentUserId, replyId, replyText);
|
||||
EditReplyDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.edit_reply_delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
feedUtils.deleteReply(activity, story, commentUserId, replyId);
|
||||
feedUtils.deleteReply(requireContext(), story, commentUserId, replyId);
|
||||
EditReplyDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.newsblur.fragment;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
@ -11,7 +10,6 @@ import android.os.Bundle;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
@ -58,15 +56,13 @@ public class FeedIntelTrainerFragment extends DialogFragment {
|
|||
fs = (FeedSet) getArguments().getSerializable("feedset");
|
||||
classifier = dbHelper.getClassifierForFeed(feed.feedId);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
LayoutInflater inflater = LayoutInflater.from(activity);
|
||||
View v = inflater.inflate(R.layout.dialog_trainfeed, null);
|
||||
View v = getLayoutInflater().inflate(R.layout.dialog_trainfeed, null);
|
||||
binding = DialogTrainfeedBinding.bind(v);
|
||||
|
||||
// display known title classifiers
|
||||
for (Map.Entry<String, Integer> rule : classifier.title.entrySet()) {
|
||||
View row = inflater.inflate(R.layout.include_intel_row, null);
|
||||
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
|
||||
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView label = row.findViewById(R.id.intel_row_label);
|
||||
label.setText(rule.getKey());
|
||||
UIUtils.setupIntelDialogRow(row, classifier.title, rule.getKey());
|
||||
binding.existingTitleIntelContainer.addView(row);
|
||||
|
@ -82,8 +78,8 @@ public class FeedIntelTrainerFragment extends DialogFragment {
|
|||
}
|
||||
}
|
||||
for (String tag : allTags) {
|
||||
View row = inflater.inflate(R.layout.include_intel_row, null);
|
||||
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
|
||||
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView label = row.findViewById(R.id.intel_row_label);
|
||||
label.setText(tag);
|
||||
UIUtils.setupIntelDialogRow(row, classifier.tags, tag);
|
||||
binding.existingTagIntelContainer.addView(row);
|
||||
|
@ -99,8 +95,8 @@ public class FeedIntelTrainerFragment extends DialogFragment {
|
|||
}
|
||||
}
|
||||
for (String author : allAuthors) {
|
||||
View rowAuthor = inflater.inflate(R.layout.include_intel_row, null);
|
||||
TextView labelAuthor = (TextView) rowAuthor.findViewById(R.id.intel_row_label);
|
||||
View rowAuthor = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView labelAuthor = rowAuthor.findViewById(R.id.intel_row_label);
|
||||
labelAuthor.setText(author);
|
||||
UIUtils.setupIntelDialogRow(rowAuthor, classifier.authors, author);
|
||||
binding.existingAuthorIntelContainer.addView(rowAuthor);
|
||||
|
@ -108,13 +104,13 @@ public class FeedIntelTrainerFragment extends DialogFragment {
|
|||
if (allAuthors.size() < 1) binding.intelAuthorHeader.setVisibility(View.GONE);
|
||||
|
||||
// for feel-level intel, the label is the title and the intel identifier is the feed ID
|
||||
View rowFeed = inflater.inflate(R.layout.include_intel_row, null);
|
||||
TextView labelFeed = (TextView) rowFeed.findViewById(R.id.intel_row_label);
|
||||
View rowFeed = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView labelFeed = rowFeed.findViewById(R.id.intel_row_label);
|
||||
labelFeed.setText(feed.title);
|
||||
UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, feed.feedId);
|
||||
binding.existingFeedIntelContainer.addView(rowFeed);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
builder.setTitle(R.string.feed_intel_dialog_title);
|
||||
builder.setView(v);
|
||||
|
||||
|
@ -127,7 +123,7 @@ public class FeedIntelTrainerFragment extends DialogFragment {
|
|||
builder.setPositiveButton(R.string.dialog_story_intel_save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
feedUtils.updateClassifier(feed.feedId, classifier, fs, activity);
|
||||
feedUtils.updateClassifier(feed.feedId, classifier, fs, requireContext());
|
||||
FeedIntelTrainerFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ public class FeedSelectorFragment extends Fragment implements StateChangedListen
|
|||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View v = inflater.inflate(R.layout.fragment_intelligenceselector, null);
|
||||
button = (StateToggleButton) v.findViewById(R.id.fragment_intelligence_statebutton);
|
||||
button = v.findViewById(R.id.fragment_intelligence_statebutton);
|
||||
button.setStateListener(this);
|
||||
return v;
|
||||
}
|
||||
|
|
|
@ -181,7 +181,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
binding.folderfeedList.setOnGroupCollapseListener(this);
|
||||
binding.folderfeedList.setOnGroupExpandListener(this);
|
||||
|
||||
adapter.listBackref = new WeakReference(binding.folderfeedList); // see note in adapter about backref
|
||||
adapter.listBackref = new WeakReference<>(binding.folderfeedList); // see note in adapter about backref
|
||||
binding.folderfeedList.setAdapter(adapter);
|
||||
|
||||
// Main activity needs to listen for scrolls to prevent refresh from firing unnecessarily
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.activity.ItemsList;
|
||||
import com.newsblur.activity.NbActivity;
|
||||
|
@ -41,7 +42,6 @@ import com.newsblur.util.StoryListStyle;
|
|||
import com.newsblur.util.ThumbnailStyle;
|
||||
import com.newsblur.util.UIUtils;
|
||||
import com.newsblur.util.ViewUtils;
|
||||
import com.newsblur.view.ProgressThrobber;
|
||||
import com.newsblur.viewModel.StoriesViewModel;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
@ -67,7 +67,15 @@ public class ItemSetFragment extends NbFragment {
|
|||
|
||||
private static final String BUNDLE_GRIDSTATE = "gridstate";
|
||||
|
||||
protected boolean cursorSeenYet = false; // have we yet seen a valid cursor for our particular feedset?
|
||||
private boolean cursorSeenYet = false; // have we yet seen a valid cursor for our particular feedset?
|
||||
|
||||
/**
|
||||
* Flag used to ensure that when the adapter resumes,
|
||||
* it omits any new stories that would disrupt the current order and cause the list to
|
||||
* unexpectedly jump, thereby preserving the scroll position. This flag specifically helps
|
||||
* manage the insertion of new stories that have been backfilled according to their timestamps.
|
||||
*/
|
||||
private boolean skipBackFillingStories = false;
|
||||
|
||||
private int itemGridWidthPx = 0;
|
||||
private int columnCount;
|
||||
|
@ -84,7 +92,7 @@ public class ItemSetFragment extends NbFragment {
|
|||
// R.id.top_loading_throb
|
||||
|
||||
// loading indicator for when stories are present and fresh (at bottom of list)
|
||||
protected ProgressThrobber bottomProgressView;
|
||||
protected LinearProgressIndicator bottomProgressView;
|
||||
|
||||
// the fleuron has padding that can't be calculated until after layout, but only changes
|
||||
// rarely thereafter
|
||||
|
@ -119,6 +127,7 @@ public class ItemSetFragment extends NbFragment {
|
|||
// readings and cause zero-index refreshes, wasting massive cycles. hold the refresh logic
|
||||
// until the loaders reset
|
||||
cursorSeenYet = false;
|
||||
skipBackFillingStories = true;
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
|
@ -149,14 +158,11 @@ public class ItemSetFragment extends NbFragment {
|
|||
// disable the throbbers if animations are going to have a zero time scale
|
||||
boolean isDisableAnimations = ViewUtils.isPowerSaveMode(requireContext());
|
||||
|
||||
int[] colorsArray = UIUtils.getLoadingColorsArray(requireContext());
|
||||
binding.topLoadingThrob.setEnabled(!isDisableAnimations);
|
||||
binding.topLoadingThrob.setColors(colorsArray);
|
||||
binding.topLoadingIndicator.setEnabled(!isDisableAnimations);
|
||||
|
||||
View footerView = inflater.inflate(R.layout.row_loading_throbber, null);
|
||||
bottomProgressView = footerView.findViewById(R.id.itemlist_loading_throb);
|
||||
View footerView = inflater.inflate(R.layout.row_loading_indicator, null);
|
||||
bottomProgressView = footerView.findViewById(R.id.itemlist_loading);
|
||||
bottomProgressView.setEnabled(!isDisableAnimations);
|
||||
bottomProgressView.setColors(colorsArray);
|
||||
|
||||
fleuronBinding.getRoot().setVisibility(View.INVISIBLE);
|
||||
fleuronBinding.containerSubscribe.setOnClickListener(view -> UIUtils.startSubscriptionActivity(requireContext()));
|
||||
|
@ -280,7 +286,7 @@ public class ItemSetFragment extends NbFragment {
|
|||
}
|
||||
|
||||
protected void updateAdapter(@Nullable Cursor cursor) {
|
||||
adapter.swapCursor(cursor, binding.itemgridfragmentGrid, gridState);
|
||||
adapter.swapCursor(cursor, binding.itemgridfragmentGrid, gridState, skipBackFillingStories);
|
||||
gridState = null;
|
||||
adapter.updateFeedSet(getFeedSet());
|
||||
if ((cursor != null) && (cursor.getCount() > 0)) {
|
||||
|
@ -318,7 +324,7 @@ public class ItemSetFragment extends NbFragment {
|
|||
if (cursorSeenYet && adapter.getRawStoryCount() > 0 && UIUtils.needsSubscriptionAccess(requireContext(), getFeedSet())) {
|
||||
fleuronBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
fleuronBinding.containerSubscribe.setVisibility(View.VISIBLE);
|
||||
binding.topLoadingThrob.setVisibility(View.INVISIBLE);
|
||||
binding.topLoadingIndicator.setVisibility(View.INVISIBLE);
|
||||
bottomProgressView.setVisibility(View.INVISIBLE);
|
||||
fleuronResized = false;
|
||||
return;
|
||||
|
@ -330,10 +336,10 @@ public class ItemSetFragment extends NbFragment {
|
|||
binding.emptyViewImage.setVisibility(View.INVISIBLE);
|
||||
|
||||
if (NBSyncService.isFeedSetStoriesFresh(getFeedSet())) {
|
||||
binding.topLoadingThrob.setVisibility(View.INVISIBLE);
|
||||
binding.topLoadingIndicator.setVisibility(View.INVISIBLE);
|
||||
bottomProgressView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.topLoadingThrob.setVisibility(View.VISIBLE);
|
||||
binding.topLoadingIndicator.setVisibility(View.VISIBLE);
|
||||
bottomProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
fleuronBinding.getRoot().setVisibility(View.INVISIBLE);
|
||||
|
@ -347,7 +353,7 @@ public class ItemSetFragment extends NbFragment {
|
|||
binding.emptyViewText.setTypeface(binding.emptyViewText.getTypeface(), Typeface.NORMAL);
|
||||
binding.emptyViewImage.setVisibility(View.VISIBLE);
|
||||
|
||||
binding.topLoadingThrob.setVisibility(View.INVISIBLE);
|
||||
binding.topLoadingIndicator.setVisibility(View.INVISIBLE);
|
||||
bottomProgressView.setVisibility(View.INVISIBLE);
|
||||
if (cursorSeenYet && NBSyncService.isFeedSetExhausted(getFeedSet()) && (adapter.getRawStoryCount() > 0)) {
|
||||
fleuronBinding.containerSubscribe.setVisibility(View.GONE);
|
||||
|
|
|
@ -13,7 +13,6 @@ import com.newsblur.database.BlurDatabaseHelper
|
|||
import com.newsblur.databinding.LoginasDialogBinding
|
||||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.util.PrefsUtils
|
||||
import com.newsblur.util.UIUtils
|
||||
import com.newsblur.util.executeAsyncTask
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
@ -50,7 +49,7 @@ class LoginAsDialogFragment : DialogFragment() {
|
|||
val startMain = Intent(requireContext(), Main::class.java)
|
||||
requireContext().startActivity(startMain)
|
||||
} else {
|
||||
UIUtils.safeToast(requireActivity(), "Login as $username failed", Toast.LENGTH_LONG)
|
||||
Toast.makeText(requireActivity(), "Login as $username failed", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -8,14 +8,13 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.newsblur.R
|
||||
import com.newsblur.activity.Profile
|
||||
import com.newsblur.database.BlurDatabaseHelper
|
||||
import com.newsblur.databinding.FragmentProfileactivityBinding
|
||||
import com.newsblur.databinding.RowLoadingThrobberBinding
|
||||
import com.newsblur.databinding.RowLoadingIndicatorBinding
|
||||
import com.newsblur.di.IconLoader
|
||||
import com.newsblur.domain.ActivityDetails
|
||||
import com.newsblur.domain.UserDetails
|
||||
|
@ -39,7 +38,7 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
|
|||
lateinit var iconLoader: ImageLoader
|
||||
|
||||
private lateinit var binding: FragmentProfileactivityBinding
|
||||
private lateinit var footerBinding: RowLoadingThrobberBinding
|
||||
private lateinit var footerBinding: RowLoadingIndicatorBinding
|
||||
|
||||
private var adapter: ActivityDetailsAdapter? = null
|
||||
private var user: UserDetails? = null
|
||||
|
@ -47,15 +46,12 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_profileactivity, null)
|
||||
binding = FragmentProfileactivityBinding.bind(view)
|
||||
val colorsArray = UIUtils.getLoadingColorsArray(requireContext())
|
||||
|
||||
binding.emptyViewLoadingThrob.setColors(*colorsArray)
|
||||
binding.profileDetailsActivitylist.setFooterDividersEnabled(false)
|
||||
binding.profileDetailsActivitylist.emptyView = binding.emptyView
|
||||
|
||||
val footerView = inflater.inflate(R.layout.row_loading_throbber, null)
|
||||
footerBinding = RowLoadingThrobberBinding.bind(footerView)
|
||||
footerBinding.itemlistLoadingThrob.setColors(*colorsArray)
|
||||
val footerView = inflater.inflate(R.layout.row_loading_indicator, null)
|
||||
footerBinding = RowLoadingIndicatorBinding.bind(footerView)
|
||||
binding.profileDetailsActivitylist.addFooterView(footerView, null, false)
|
||||
if (adapter != null) {
|
||||
displayActivities()
|
||||
|
@ -80,8 +76,8 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
|
|||
private fun loadPage(pageNumber: Int) {
|
||||
lifecycleScope.executeAsyncTask(
|
||||
onPreExecute = {
|
||||
binding.emptyViewLoadingThrob.visibility = View.VISIBLE
|
||||
footerBinding.itemlistLoadingThrob.visibility = View.VISIBLE
|
||||
binding.emptyViewLoading.visibility = View.VISIBLE
|
||||
footerBinding.itemlistLoading.visibility = View.VISIBLE
|
||||
},
|
||||
doInBackground = {
|
||||
// For the logged in user user.userId is null.
|
||||
|
@ -106,8 +102,8 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
|
|||
adapter!!.add(activity)
|
||||
}
|
||||
adapter!!.notifyDataSetChanged()
|
||||
binding.emptyViewLoadingThrob.visibility = View.GONE
|
||||
footerBinding.itemlistLoadingThrob.visibility = View.GONE
|
||||
binding.emptyViewLoading.visibility = View.GONE
|
||||
footerBinding.itemlistLoading.visibility = View.GONE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,10 +33,10 @@ import com.newsblur.domain.Story
|
|||
import com.newsblur.domain.UserDetails
|
||||
import com.newsblur.keyboard.KeyboardManager
|
||||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_INTEL
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_TEXT
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_INTEL
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_SOCIAL
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_STORY
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_TEXT
|
||||
import com.newsblur.service.OriginalTextService
|
||||
import com.newsblur.util.*
|
||||
import com.newsblur.util.PrefConstants.ThemeValue
|
||||
|
@ -422,7 +422,9 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
true
|
||||
}
|
||||
R.id.menu_go_to_feed -> {
|
||||
FeedItemsList.startActivity(context, fs, dbHelper.getFeed(story!!.feedId), null, null)
|
||||
val feed = dbHelper.getFeed(story!!.feedId)
|
||||
val fs = FeedSet.singleFeed(feed.feedId)
|
||||
FeedItemsList.startActivity(requireContext(), fs, feed, null, null)
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.newsblur.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
@ -8,7 +7,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
@ -58,12 +56,10 @@ public class RenameDialogFragment extends DialogFragment {
|
|||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
LayoutInflater inflater = LayoutInflater.from(activity);
|
||||
View v = inflater.inflate(R.layout.dialog_rename, null);
|
||||
View v = getLayoutInflater().inflate(R.layout.dialog_rename, null);
|
||||
final DialogRenameBinding binding = DialogRenameBinding.bind(v);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
builder.setView(v);
|
||||
builder.setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
|
@ -78,7 +74,7 @@ public class RenameDialogFragment extends DialogFragment {
|
|||
builder.setPositiveButton(R.string.feed_name_save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
feedUtils.renameFeed(activity, feed.feedId, binding.inputName.getText().toString());
|
||||
feedUtils.renameFeed(requireContext(), feed.feedId, binding.inputName.getText().toString());
|
||||
RenameDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
@ -94,7 +90,7 @@ public class RenameDialogFragment extends DialogFragment {
|
|||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
String newFolderName = binding.inputName.getText().toString();
|
||||
if (TextUtils.isEmpty(newFolderName)) {
|
||||
Toast.makeText(activity, R.string.add_folder_name, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(requireContext(), R.string.add_folder_name, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -102,7 +98,7 @@ public class RenameDialogFragment extends DialogFragment {
|
|||
if (!TextUtils.isEmpty(folderParentName) && !folderParentName.equals(AppConstants.ROOT_FOLDER)) {
|
||||
inFolder = folderParentName;
|
||||
}
|
||||
feedUtils.renameFolder(folderName, newFolderName, inFolder, activity);
|
||||
feedUtils.renameFolder(folderName, newFolderName, inFolder, requireContext());
|
||||
RenameDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package com.newsblur.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
|
@ -47,21 +45,18 @@ public class ReplyDialogFragment extends DialogFragment {
|
|||
story = (Story) getArguments().getSerializable(STORY);
|
||||
commentUserId = getArguments().getString(COMMENT_USER_ID);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
String shareString = getResources().getString(R.string.reply_to);
|
||||
builder.setTitle(String.format(shareString, getArguments().getString(COMMENT_USERNAME)));
|
||||
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(activity);
|
||||
View replyView = layoutInflater.inflate(R.layout.reply_dialog, null);
|
||||
View replyView = getLayoutInflater().inflate(R.layout.reply_dialog, null);
|
||||
builder.setView(replyView);
|
||||
final EditText reply = (EditText) replyView.findViewById(R.id.reply_field);
|
||||
final EditText reply = replyView.findViewById(R.id.reply_field);
|
||||
|
||||
builder.setPositiveButton(R.string.alert_dialog_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
feedUtils.replyToComment(story.id, story.feedId, commentUserId, reply.getText().toString(), activity);
|
||||
feedUtils.replyToComment(story.id, story.feedId, commentUserId, reply.getText().toString(), requireContext());
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.newsblur.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
@ -8,7 +7,6 @@ import android.os.Bundle;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
|
@ -53,9 +51,8 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
story = (Story) getArguments().getSerializable(STORY);
|
||||
user = PrefsUtils.getUserDetails(activity);
|
||||
user = PrefsUtils.getUserDetails(requireContext());
|
||||
sourceUserId = getArguments().getString(SOURCE_USER_ID);
|
||||
|
||||
boolean hasBeenShared = false;
|
||||
|
@ -70,13 +67,12 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
previousComment = dbHelper.getComment(story.id, user.id);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
builder.setTitle(String.format(getResources().getString(R.string.share_save_newsblur), UIUtils.fromHtml(story.title)));
|
||||
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(activity);
|
||||
View replyView = layoutInflater.inflate(R.layout.share_dialog, null);
|
||||
View replyView = getLayoutInflater().inflate(R.layout.share_dialog, null);
|
||||
builder.setView(replyView);
|
||||
commentEditText = (EditText) replyView.findViewById(R.id.comment_field);
|
||||
commentEditText = replyView.findViewById(R.id.comment_field);
|
||||
|
||||
int positiveButtonText = R.string.share_this_story;
|
||||
int negativeButtonText = R.string.alert_dialog_cancel;
|
||||
|
@ -92,7 +88,7 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
String shareComment = commentEditText.getText().toString();
|
||||
feedUtils.shareStory(story, shareComment, sourceUserId, activity);
|
||||
feedUtils.shareStory(story, shareComment, sourceUserId, requireContext());
|
||||
ShareDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
@ -101,7 +97,7 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
feedUtils.unshareStory(story, activity);
|
||||
feedUtils.unshareStory(story, requireContext());
|
||||
ShareDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -110,7 +110,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
for (Map.Entry<String, Integer> rule : classifier.title.entrySet()) {
|
||||
if (story.title.indexOf(rule.getKey()) >= 0) {
|
||||
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
|
||||
TextView label = row.findViewById(R.id.intel_row_label);
|
||||
label.setText(rule.getKey());
|
||||
UIUtils.setupIntelDialogRow(row, classifier.title, rule.getKey());
|
||||
binding.existingTitleIntelContainer.addView(row);
|
||||
|
@ -120,7 +120,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
// list all tags for this story, trained or not
|
||||
for (String tag : story.tags) {
|
||||
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
|
||||
TextView label = row.findViewById(R.id.intel_row_label);
|
||||
label.setText(tag);
|
||||
UIUtils.setupIntelDialogRow(row, classifier.tags, tag);
|
||||
binding.existingTagIntelContainer.addView(row);
|
||||
|
@ -130,7 +130,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
// there is a single author per story
|
||||
if (!TextUtils.isEmpty(story.authors)) {
|
||||
View rowAuthor = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView labelAuthor = (TextView) rowAuthor.findViewById(R.id.intel_row_label);
|
||||
TextView labelAuthor = rowAuthor.findViewById(R.id.intel_row_label);
|
||||
labelAuthor.setText(story.authors);
|
||||
UIUtils.setupIntelDialogRow(rowAuthor, classifier.authors, story.authors);
|
||||
binding.existingAuthorIntelContainer.addView(rowAuthor);
|
||||
|
@ -141,7 +141,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
// there is a single feed to be trained, but it is a bit odd in that the label is the title and
|
||||
// the intel identifier is the feed ID
|
||||
View rowFeed = getLayoutInflater().inflate(R.layout.include_intel_row, null);
|
||||
TextView labelFeed = (TextView) rowFeed.findViewById(R.id.intel_row_label);
|
||||
TextView labelFeed = rowFeed.findViewById(R.id.intel_row_label);
|
||||
labelFeed.setText(feedUtils.getFeedTitle(story.feedId));
|
||||
UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, story.feedId);
|
||||
binding.existingFeedIntelContainer.addView(rowFeed);
|
||||
|
|
|
@ -59,7 +59,7 @@ class StoryUserTagsFragment : DialogFragment(), TagsAdapter.OnTagClickListener {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
storyUserTagsViewModel = ViewModelProvider(this).get(StoryUserTagsViewModel::class.java)
|
||||
storyUserTagsViewModel = ViewModelProvider(this)[StoryUserTagsViewModel::class.java]
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
|
|
@ -88,7 +88,6 @@ public class APIResponse {
|
|||
} catch (IOException ioe) {
|
||||
com.newsblur.util.Log.e(this.getClass().getName(), "Error (" + ioe.getMessage() + ") calling " + request.url().toString(), ioe);
|
||||
this.isError = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,7 +115,7 @@ public class APIResponse {
|
|||
try {
|
||||
T response = classOfT.newInstance();
|
||||
response.isProtocolError = true;
|
||||
return ((T) response);
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
// this should never fail unless the constructor of the base response bean fails
|
||||
Log.wtf(this.getClass().getName(), "Failed to load class: " + classOfT);
|
||||
|
|
|
@ -22,16 +22,16 @@ public class ClassifierMapTypeAdapter implements JsonDeserializer<Map<String,Cla
|
|||
@Override
|
||||
public Map<String,Classifier> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
|
||||
|
||||
Map<String,Classifier> result = new HashMap<String,Classifier>();
|
||||
Map<String,Classifier> result = new HashMap<>();
|
||||
|
||||
if (jsonElement.isJsonObject()) {
|
||||
JsonObject o = jsonElement.getAsJsonObject();
|
||||
if (o.get("authors") != null) { // this is our hint that this is a bare classifiers object
|
||||
Classifier c = (Classifier) jsonDeserializationContext.deserialize(jsonElement, Classifier.class);
|
||||
Classifier c = jsonDeserializationContext.deserialize(jsonElement, Classifier.class);
|
||||
result.put( "-1", c);
|
||||
} else { // otherwise, we have a map of IDs to classifiers
|
||||
for (Map.Entry<String, JsonElement> entry : o.entrySet()) {
|
||||
Classifier c = (Classifier) jsonDeserializationContext.deserialize(entry.getValue(), Classifier.class);
|
||||
Classifier c = jsonDeserializationContext.deserialize(entry.getValue(), Classifier.class);
|
||||
result.put(entry.getKey(), c);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@ import com.google.gson.JsonDeserializer;
|
|||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import com.newsblur.NbApplication;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.network.APIConstants;
|
||||
import com.newsblur.util.StoryUtil;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
@ -43,8 +45,15 @@ public class StoryTypeAdapter implements JsonDeserializer<Story> {
|
|||
story.timestamp = story.timestamp * 1000;
|
||||
story.starredTimestamp = story.starredTimestamp * 1000;
|
||||
|
||||
// due to android.os.TransactionTooLargeException and
|
||||
// android.database.sqlite.SQLiteBlobTooBigException
|
||||
// truncate the story's content in case it's large
|
||||
if (!NbApplication.isAppForeground()) {
|
||||
story.content = StoryUtil.truncateContent(story.content);
|
||||
}
|
||||
|
||||
// replace http image urls with https
|
||||
if (httpSniff.matcher(story.content).find() && story.secureImageUrls != null && story.secureImageUrls.size() > 0) {
|
||||
if (httpSniff.matcher(story.content).find() && story.secureImageUrls != null && !story.secureImageUrls.isEmpty()) {
|
||||
for (String url : story.secureImageUrls.keySet()) {
|
||||
if (httpSniff.matcher(url).find()) {
|
||||
String secureUrl = story.secureImageUrls.get(url);
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
package com.newsblur.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
abstract class NBSyncReceiver : BroadcastReceiver() {
|
||||
|
||||
abstract fun handleUpdateType(updateType: Int)
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == NB_SYNC_ACTION) {
|
||||
handleUpdateType(intent.getIntExtra(NB_SYNC_UPDATE_TYPE, 0))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NB_SYNC_ACTION = "nb.sync.action"
|
||||
const val NB_SYNC_ERROR_MESSAGE = "nb_sync_error_msg"
|
||||
const val NB_SYNC_UPDATE_TYPE = "nb_sync_update_type"
|
||||
|
||||
const val UPDATE_DB_READY = 1 shl 0
|
||||
const val UPDATE_METADATA = 1 shl 1
|
||||
const val UPDATE_STORY = 1 shl 2
|
||||
const val UPDATE_SOCIAL = 1 shl 3
|
||||
const val UPDATE_INTEL = 1 shl 4
|
||||
const val UPDATE_STATUS = 1 shl 5
|
||||
const val UPDATE_TEXT = 1 shl 6
|
||||
const val UPDATE_REBUILD = 1 shl 7
|
||||
}
|
||||
}
|
|
@ -13,11 +13,11 @@ import com.newsblur.NbApplication;
|
|||
import com.newsblur.R;
|
||||
import com.newsblur.database.BlurDatabaseHelper;
|
||||
import static com.newsblur.database.BlurDatabaseHelper.closeQuietly;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_DB_READY;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_METADATA;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_DB_READY;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_METADATA;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_REBUILD;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_STORY;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
@ -47,10 +47,7 @@ import com.newsblur.util.NetworkUtils;
|
|||
import com.newsblur.util.NotificationUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadingAction;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.util.UIUtils;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -272,6 +269,12 @@ public class NBSyncService extends JobService {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkChanged(@NonNull JobParameters params) {
|
||||
super.onNetworkChanged(params);
|
||||
com.newsblur.util.Log.d(this, "onNetworkChanged");
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the actual work of syncing.
|
||||
*/
|
||||
|
@ -1160,7 +1163,7 @@ public class NBSyncService extends JobService {
|
|||
dbHelper.prepareReadingSession(fs, cursorFilters.getStateFilter(), cursorFilters.getReadFilter());
|
||||
// note which feedset we are loading so we can trigger another reset when it changes
|
||||
dbHelper.setSessionFeedSet(fs);
|
||||
UIUtils.syncUpdateStatus(context, UPDATE_STORY | UPDATE_STATUS);
|
||||
NbSyncManager.submitUpdate(UPDATE_STORY | UPDATE_STATUS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1270,20 +1273,10 @@ public class NBSyncService extends JobService {
|
|||
}
|
||||
|
||||
protected void sendSyncUpdate(int update) {
|
||||
Intent i = new Intent(NBSyncReceiver.NB_SYNC_ACTION);
|
||||
i.putExtra(NBSyncReceiver.NB_SYNC_UPDATE_TYPE, update);
|
||||
broadcastSync(i);
|
||||
NbSyncManager.submitUpdate(update);
|
||||
}
|
||||
|
||||
protected void sendToastError(@NonNull String message) {
|
||||
Intent i = new Intent(NBSyncReceiver.NB_SYNC_ACTION);
|
||||
i.putExtra(NBSyncReceiver.NB_SYNC_ERROR_MESSAGE, message);
|
||||
broadcastSync(i);
|
||||
}
|
||||
|
||||
private void broadcastSync(@NonNull Intent intent) {
|
||||
if (NbApplication.isAppForeground()) {
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
NbSyncManager.submitError(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package com.newsblur.service
|
||||
|
||||
import com.newsblur.util.NBScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NbSyncManager {
|
||||
|
||||
const val UPDATE_DB_READY = 1 shl 0
|
||||
const val UPDATE_METADATA = 1 shl 1
|
||||
const val UPDATE_STORY = 1 shl 2
|
||||
const val UPDATE_SOCIAL = 1 shl 3
|
||||
const val UPDATE_INTEL = 1 shl 4
|
||||
const val UPDATE_STATUS = 1 shl 5
|
||||
const val UPDATE_TEXT = 1 shl 6
|
||||
const val UPDATE_REBUILD = 1 shl 7
|
||||
|
||||
private val _state = MutableSharedFlow<NBSync>()
|
||||
val state = _state.asSharedFlow()
|
||||
|
||||
@JvmStatic
|
||||
fun submitUpdate(type: Int) = submit(NBSync.Update(type))
|
||||
|
||||
@JvmStatic
|
||||
fun submitError(msg: String) = submit(NBSync.Error(msg))
|
||||
|
||||
private fun submit(nbSync: NBSync) {
|
||||
NBScope.launch(Dispatchers.IO) {
|
||||
_state.emit(nbSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class NBSync {
|
||||
|
||||
data class Error(
|
||||
val msg: String,
|
||||
) : NBSync()
|
||||
|
||||
data class Update(
|
||||
val type: Int,
|
||||
) : NBSync()
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package com.newsblur.service;
|
||||
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_TEXT;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_TEXT;
|
||||
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.network.domain.StoryTextResponse;
|
||||
|
|
|
@ -36,7 +36,7 @@ abstract class SubService(
|
|||
|
||||
if (isActive) {
|
||||
parent.checkCompletion()
|
||||
parent.sendSyncUpdate(NBSyncReceiver.UPDATE_STATUS)
|
||||
parent.sendSyncUpdate(NbSyncManager.UPDATE_STATUS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,8 +139,8 @@ public class UnreadsService extends SubService {
|
|||
boolean isTextPrefetchEnabled = PrefsUtils.isTextPrefetchEnabled(parent);
|
||||
if (! (isOfflineEnabled || isEnableNotifications)) return;
|
||||
|
||||
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
List<String> hashSkips = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
List<String> hashBatch = new ArrayList<>(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
List<String> hashSkips = new ArrayList<>(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
batchloop: for (String hash : StoryHashQueue) {
|
||||
if( isOfflineEnabled ||
|
||||
(isEnableNotifications && notifyFeeds.contains(FeedUtils.inferFeedId(hash))) ) {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package com.newsblur.util
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.newsblur.R
|
||||
import com.newsblur.network.APIConstants
|
||||
|
||||
object FileDownloader {
|
||||
|
||||
fun exportOpml(context: Context): Long {
|
||||
val manager = context.getSystemService(DownloadManager::class.java)
|
||||
val url = APIConstants.buildUrl(APIConstants.PATH_EXPORT_OPML)
|
||||
val userName = PrefsUtils.getUserName(context)
|
||||
val cookie = PrefsUtils.getCookie(context)
|
||||
|
||||
val file = StringBuilder().apply {
|
||||
append(context.getString(R.string.newsbluropml))
|
||||
userName?.let { append("-$userName") }
|
||||
append(".xml")
|
||||
}.toString()
|
||||
|
||||
val request = DownloadManager.Request(url.toUri())
|
||||
.setMimeType(MimeTypeMap.getSingleton().getMimeTypeFromExtension(".xml"))
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.addRequestHeader("Cookie", cookie)
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, file)
|
||||
.setTitle(context.getString(R.string.newsblur_opml))
|
||||
return manager.enqueue(request)
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadCompleteReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
|
||||
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
|
||||
if (id == expectedFileDownloadId) {
|
||||
context?.let {
|
||||
val msg = "${it.getString(R.string.newsblur_opml)} download completed"
|
||||
Toast.makeText(it, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
var expectedFileDownloadId: Long? = null
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ object FeedExt {
|
|||
|
||||
fun Feed.disableNotification() {
|
||||
notificationFilter = null
|
||||
disableNotificationType(NOTIFY_ANDROID)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -39,10 +40,12 @@ object FeedExt {
|
|||
|
||||
fun Feed.setNotifyFocus() {
|
||||
notificationFilter = Feed.NOTIFY_FILTER_FOCUS
|
||||
enableNotificationType(NOTIFY_ANDROID)
|
||||
}
|
||||
|
||||
fun Feed.setNotifyUnread() {
|
||||
notificationFilter = Feed.NOTIFY_FILTER_UNREAD
|
||||
enableNotificationType(NOTIFY_ANDROID)
|
||||
}
|
||||
|
||||
private fun Feed.isNotify(type: String): Boolean = notificationTypes?.contains(type) ?: false
|
||||
|
|
|
@ -80,7 +80,7 @@ public class FeedSet implements Serializable {
|
|||
*/
|
||||
public static FeedSet allFeeds() {
|
||||
FeedSet fs = new FeedSet();
|
||||
fs.feeds = Collections.EMPTY_SET;
|
||||
fs.feeds = Collections.emptySet();
|
||||
return fs;
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ public class FeedSet implements Serializable {
|
|||
*/
|
||||
public static FeedSet allSaved() {
|
||||
FeedSet fs = new FeedSet();
|
||||
fs.savedTags = Collections.EMPTY_SET;
|
||||
fs.savedTags = Collections.emptySet();
|
||||
return fs;
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ public class FeedSet implements Serializable {
|
|||
*/
|
||||
public static FeedSet singleSavedTag(String tag) {
|
||||
FeedSet fs = new FeedSet();
|
||||
fs.savedTags = new HashSet<String>(1);
|
||||
fs.savedTags = new HashSet<>(1);
|
||||
fs.savedTags.add(tag);
|
||||
fs.savedTags = Collections.unmodifiableSet(fs.savedTags);
|
||||
return fs;
|
||||
|
@ -146,7 +146,7 @@ public class FeedSet implements Serializable {
|
|||
*/
|
||||
public static FeedSet allSocialFeeds() {
|
||||
FeedSet fs = new FeedSet();
|
||||
fs.socialFeeds = Collections.EMPTY_MAP;
|
||||
fs.socialFeeds = Collections.emptyMap();
|
||||
return fs;
|
||||
}
|
||||
|
||||
|
@ -161,7 +161,7 @@ public class FeedSet implements Serializable {
|
|||
fs.feeds.addAll(feedIds);
|
||||
fs.feeds = Collections.unmodifiableSet(fs.feeds);
|
||||
} else {
|
||||
fs.feeds = Collections.EMPTY_SET;
|
||||
fs.feeds = Collections.emptySet();
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import com.newsblur.NbApplication
|
||||
import com.newsblur.R
|
||||
import com.newsblur.activity.NbActivity
|
||||
import com.newsblur.database.BlurDatabaseHelper
|
||||
|
@ -13,14 +14,15 @@ import com.newsblur.domain.Story
|
|||
import com.newsblur.fragment.ReadingActionConfirmationFragment
|
||||
import com.newsblur.network.APIConstants
|
||||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_METADATA
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
|
||||
import com.newsblur.service.NBSyncService
|
||||
import com.newsblur.service.NbSyncManager
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_METADATA
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_SOCIAL
|
||||
import com.newsblur.service.NbSyncManager.UPDATE_STORY
|
||||
import com.newsblur.service.OriginalTextService
|
||||
import com.newsblur.util.FeedExt.disableNotification
|
||||
import com.newsblur.util.FeedExt.setNotifyFocus
|
||||
import com.newsblur.util.FeedExt.setNotifyUnread
|
||||
import com.newsblur.util.UIUtils.syncUpdateStatus
|
||||
|
||||
class FeedUtils(
|
||||
private val dbHelper: BlurDatabaseHelper,
|
||||
|
@ -66,14 +68,14 @@ class FeedUtils(
|
|||
doInBackground = {
|
||||
val ra = if (saved) ReadingAction.saveStory(storyHash, userTags) else ReadingAction.unsaveStory(storyHash)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_STORY)
|
||||
syncUpdateStatus(UPDATE_STORY)
|
||||
dbHelper.enqueueAction(ra)
|
||||
triggerSync(context)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteSavedSearch(feedId: String?, query: String?, context: Context) {
|
||||
fun deleteSavedSearch(feedId: String?, query: String?) {
|
||||
NBScope.executeAsyncTask(
|
||||
doInBackground = {
|
||||
apiManager.deleteSearch(feedId, query)
|
||||
|
@ -81,7 +83,7 @@ class FeedUtils(
|
|||
onPostExecute = { newsBlurResponse ->
|
||||
if (!newsBlurResponse.isError) {
|
||||
dbHelper.deleteSavedSearch(feedId, query)
|
||||
syncUpdateStatus(context, UPDATE_METADATA)
|
||||
syncUpdateStatus(UPDATE_METADATA)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -101,7 +103,7 @@ class FeedUtils(
|
|||
)
|
||||
}
|
||||
|
||||
fun deleteFeed(feedId: String?, folderName: String?, context: Context) {
|
||||
fun deleteFeed(feedId: String?, folderName: String?) {
|
||||
NBScope.executeAsyncTask(
|
||||
doInBackground = {
|
||||
apiManager.deleteFeed(feedId, folderName)
|
||||
|
@ -109,12 +111,12 @@ class FeedUtils(
|
|||
onPostExecute = {
|
||||
// TODO: we can't check result.isError() because the delete call sets the .message property on all calls. find a better error check
|
||||
dbHelper.deleteFeed(feedId)
|
||||
syncUpdateStatus(context, UPDATE_METADATA)
|
||||
syncUpdateStatus(UPDATE_METADATA)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteSocialFeed(userId: String?, context: Context) {
|
||||
fun deleteSocialFeed(userId: String?) {
|
||||
NBScope.executeAsyncTask(
|
||||
doInBackground = {
|
||||
apiManager.unfollowUser(userId)
|
||||
|
@ -122,7 +124,7 @@ class FeedUtils(
|
|||
onPostExecute = {
|
||||
// TODO: we can't check result.isError() because the delete call sets the .message property on all calls. find a better error check
|
||||
dbHelper.deleteSocialFeed(userId)
|
||||
syncUpdateStatus(context, UPDATE_METADATA)
|
||||
syncUpdateStatus(UPDATE_METADATA)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -189,7 +191,7 @@ class FeedUtils(
|
|||
|
||||
// update unread state and unread counts in the local DB
|
||||
val impactedFeeds = dbHelper.setStoryReadState(story, read)
|
||||
syncUpdateStatus(context, UPDATE_STORY)
|
||||
syncUpdateStatus(UPDATE_STORY)
|
||||
|
||||
NBSyncService.addRecountCandidates(impactedFeeds)
|
||||
triggerSync(context)
|
||||
|
@ -309,7 +311,7 @@ class FeedUtils(
|
|||
doInBackground = {
|
||||
dbHelper.enqueueAction(ra)
|
||||
val impact = ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, impact)
|
||||
syncUpdateStatus(impact)
|
||||
triggerSync(context)
|
||||
}
|
||||
)
|
||||
|
@ -333,7 +335,9 @@ class FeedUtils(
|
|||
fun sendStoryFull(story: Story?, context: Context) {
|
||||
if (story == null) return
|
||||
var body = getStoryText(story.storyHash)
|
||||
if (TextUtils.isEmpty(body)) body = getStoryContent(story.storyHash)
|
||||
if (body.isNullOrEmpty() || body == OriginalTextService.NULL_STORY_TEXT) {
|
||||
body = getStoryContent(story.storyHash)
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
@ -350,7 +354,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.shareStory(story.storyHash, story.id, story.feedId, sourceUserId, comment)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
|
||||
syncUpdateStatus(UPDATE_SOCIAL or UPDATE_STORY)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -358,7 +362,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.renameFeed(feedId, newFeedName)
|
||||
dbHelper.enqueueAction(ra)
|
||||
val impact = ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, impact)
|
||||
syncUpdateStatus(impact)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -366,7 +370,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
|
||||
syncUpdateStatus(UPDATE_SOCIAL or UPDATE_STORY)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -374,7 +378,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.likeComment(story.id, commentUserId, story.feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
syncUpdateStatus(UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -382,7 +386,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.unlikeComment(story.id, commentUserId, story.feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
syncUpdateStatus(UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -390,7 +394,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.replyToComment(storyId, feedId, commentUserId, replyText)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
syncUpdateStatus(UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -398,7 +402,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.updateReply(story.id, story.feedId, commentUserId, replyId, replyText)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
syncUpdateStatus(UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -406,7 +410,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.deleteReply(story.id, story.feedId, commentUserId, replyId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
syncUpdateStatus(UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -452,7 +456,7 @@ class FeedUtils(
|
|||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
|
||||
syncUpdateStatus(context, UPDATE_METADATA)
|
||||
syncUpdateStatus(UPDATE_METADATA)
|
||||
triggerSync(context)
|
||||
}
|
||||
)
|
||||
|
@ -462,7 +466,7 @@ class FeedUtils(
|
|||
val ra = ReadingAction.instaFetch(feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_METADATA)
|
||||
syncUpdateStatus(UPDATE_METADATA)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
||||
|
@ -484,6 +488,12 @@ class FeedUtils(
|
|||
UIUtils.handleUri(context, Uri.parse(url))
|
||||
}
|
||||
|
||||
private fun syncUpdateStatus(update: Int) {
|
||||
if (NbApplication.isAppForeground) {
|
||||
NbSyncManager.submitUpdate(update)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
@ -25,7 +25,7 @@ object PendingIntentUtils {
|
|||
requestCode: Int,
|
||||
intent: Intent,
|
||||
flags: Int,
|
||||
): PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
): PendingIntent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.getBroadcast(context, requestCode, intent, flags or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||
|
|
|
@ -285,6 +285,12 @@ public class PrefsUtils {
|
|||
return user;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getUserName(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return preferences.getString(PrefConstants.USER_USERNAME, null);
|
||||
}
|
||||
|
||||
private static void saveUserImage(final Context context, String pictureUrl) {
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
|
@ -1079,8 +1085,13 @@ public class PrefsUtils {
|
|||
* which gets saved when a user is authenticated.
|
||||
*/
|
||||
public static boolean hasCookie(Context context) {
|
||||
return getCookie(context) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getCookie(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, Context.MODE_PRIVATE);
|
||||
return preferences.getString(PrefConstants.PREF_COOKIE, null) != null;
|
||||
return preferences.getString(PrefConstants.PREF_COOKIE, null);
|
||||
}
|
||||
|
||||
public static MarkStoryReadBehavior getMarkStoryReadBehavior(Context context) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package com.newsblur.util;
|
||||
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_INTEL;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_METADATA;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_SOCIAL;
|
||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_INTEL;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_METADATA;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_SOCIAL;
|
||||
import static com.newsblur.service.NbSyncManager.UPDATE_STORY;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
|
@ -18,7 +18,7 @@ import com.newsblur.network.domain.CommentResponse;
|
|||
import com.newsblur.network.domain.NewsBlurResponse;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.service.NBSyncReceiver;
|
||||
import com.newsblur.service.NbSyncManager;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -375,7 +375,7 @@ public class ReadingAction implements Serializable {
|
|||
} else {
|
||||
com.newsblur.util.Log.w(this, "failed to refresh story data after action");
|
||||
}
|
||||
impact |= NBSyncReceiver.UPDATE_SOCIAL;
|
||||
impact |= NbSyncManager.UPDATE_SOCIAL;
|
||||
}
|
||||
if (commentResponse != null) {
|
||||
result = commentResponse;
|
||||
|
@ -384,7 +384,7 @@ public class ReadingAction implements Serializable {
|
|||
} else {
|
||||
com.newsblur.util.Log.w(this, "failed to refresh comment data after action");
|
||||
}
|
||||
impact |= NBSyncReceiver.UPDATE_SOCIAL;
|
||||
impact |= NbSyncManager.UPDATE_SOCIAL;
|
||||
}
|
||||
if (result != null && impact != 0) {
|
||||
result.impactCode = impact;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue