Compare commits

...

178 Commits

Author SHA1 Message Date
cadcc56182 Updated changelog 2022-12-05 16:55:06 -06:00
619f58ad90 Updated changelog 2022-12-05 16:51:57 -06:00
c480554e01 Merge branch 'next-gen' of gitlab.mcvsoftware.com:twblue/twblue into next-gen 2022-12-05 03:00:57 -06:00
6da81a9734 Get offset from local user timezone as opposed to call get_settings on twitter's side 2022-12-05 03:00:03 -06:00
b559726535 Update method to determine custom character limit to detect changes in mastodon servers 2022-11-30 11:19:51 -06:00
768f0bc396 Merge pull request #496 from MCV-Software/mastodon
Initial Mastodon Support
2022-11-25 17:50:08 -06:00
5bbf069d61 Set updated statuses also in GUI 2022-11-25 17:43:03 -06:00
d177ef5be2 Initial support for edited statuses in Streaming API. Only Works in invisible interface for now 2022-11-25 17:30:57 -06:00
b6dd539dc6 Hide VLC log info 2022-11-25 15:29:21 -06:00
fabca1207d Fixed issue when cleaning a buffer 2022-11-24 16:36:08 -06:00
bf4c7ff7c7 Make sure item ID is included when composing a dm in mastodon 2022-11-24 15:28:39 -06:00
8d2fb59ba8 Added some fixes 2022-11-24 15:22:09 -06:00
b7497791b4 Update buffer removal code. Should be able to remove user search buffers 2022-11-21 22:09:40 -06:00
48730ead63 Keep sensitive content tag when replying to posts 2022-11-21 19:36:34 -06:00
98ecd000a9 change default soundpack for mastodon sessions and hide federated timeline by default 2022-11-21 17:28:23 -06:00
4a9098021f Avoid playing new dm's sound for now 2022-11-20 15:01:15 -06:00
8850e5fdde Removed unneeded code and fixed small typos 2022-11-20 14:54:10 -06:00
e42bd85274 Fixed shortcut to open followers timelines in the dialog 2022-11-20 13:28:43 -06:00
d3914a4e34 Implemented user timelines (statuses, followers & following). Limited support due to API 2022-11-20 02:23:12 -06:00
73de8d4f49 Updated followers sound to match the correct sounds used for twitter sessions 2022-11-19 19:37:10 -06:00
ce458a8d4d Added direct messages and followers to streaming API implementation 2022-11-19 19:36:21 -06:00
149ce51f49 early support to user streaming events 2022-11-18 16:29:53 -06:00
2fe9c35c0b Add pubsub events by using twitter's namespace to separate services 2022-11-18 13:41:12 -06:00
39fb5b4830 fixed visibility settings when sending replies 2022-11-18 13:40:02 -06:00
71ca547abe Implemented user searches 2022-11-18 10:28:45 -06:00
1c5c1067e6 Fixed KeystrokeEditor's GUI 2022-11-17 17:37:44 -06:00
df4d3eb0e6 Fix Issue that avoids removing trending topic buffers 2022-11-17 17:27:41 -06:00
927bbae1be bind user actions button to its function in user buffers' GUI 2022-11-17 16:59:48 -06:00
cbc4fd0632 Added buttons for toggling favorites and bookmarks in base buffer's GUI 2022-11-17 16:57:49 -06:00
10d4d47a17 Handle errors in authorisation for both Twitter and mastodon 2022-11-17 16:19:47 -06:00
52d64d86d8 Add sessions only after authorise returns True 2022-11-17 16:19:03 -06:00
2ab61ebee6 Create session folder only if the session is actually created successfully (authorized) 2022-11-17 16:18:30 -06:00
3ad01d1ab0 Fixed a typo 2022-11-17 12:44:28 -06:00
7835e09c5b Added oauth authorisation to mastodon session 2022-11-16 15:33:27 -06:00
4c5d2b5e04 Update persons default template 2022-11-16 13:42:31 -06:00
8f72ee97c9 Replace 'toot' term to follow mastodon changes 2022-11-16 13:28:45 -06:00
aebdcae9cf Fixed some buffer names 2022-11-16 12:55:35 -06:00
3b5ef02def fixed small name conflict in buffers 2022-11-16 12:51:58 -06:00
35599ee0ef Add newline character after every image description parsed 2022-11-16 12:40:52 -06:00
6e03578e86 Fixed lists 2022-11-16 12:35:19 -06:00
7cbf873db5 Include date and client in conversation list GUI 2022-11-16 11:40:23 -06:00
fefd88b71c Create valid account buffers even when session cannot login to the network (works for ignored sessions) 2022-11-16 11:01:52 -06:00
7c80c9d842 Improve conversation replies 2022-11-16 10:31:17 -06:00
f5e52c6387 Honor custom character limit if reported by the instance 2022-11-16 10:06:14 -06:00
4c43f82b60 changed import of twitter text parser 2022-11-15 17:23:01 -06:00
2caaaa9d87 Fix: LogOut from sessions 2022-11-15 16:15:09 -06:00
3fa39c712e Set a mastodon version for the API 2022-11-15 16:13:16 -06:00
252a93f82d save list buffers after being created 2022-11-15 16:12:49 -06:00
f5328379e8 Fixed list buffer creation 2022-11-15 13:31:11 -06:00
2e8c8c6db4 Rework sessions to be handled with unique names 2022-11-15 11:54:59 -06:00
862dbd0b8a Change session name as identifiable key for every session in TWblue 2022-11-14 17:51:27 -06:00
120da217f5 Implemented userActions for mastodon sessions 2022-11-14 12:39:32 -06:00
035de92496 Added user buffers (buffers for followers, following, blocked and muted users for now) 2022-11-13 22:17:28 -06:00
2a0e73ad33 Use the same visibility when replying to toots 2022-11-12 22:32:31 -06:00
701a557357 Add support to retrieve more conversations if needed. Sort conversation properly on updates 2022-11-12 21:28:49 -06:00
de1d94e679 update toot from server before displaying item 2022-11-12 15:26:34 -06:00
64d5b7e684 Changed mentionned people for direct in toots' visibility info 2022-11-12 15:16:53 -06:00
2c9048618f Added visibility as a variable template for toots 2022-11-12 15:16:20 -06:00
José Manuel Delicado Alcolea
d66f4a2640 Updated Windows dependencies. Switching to Python 3.10.8 2022-11-12 20:29:02 +01:00
bfbb280c27 Fixed update in mentions 2022-11-12 13:06:46 -06:00
cad7c9a9fd Added conversationListBuffer GUI 2022-11-12 13:06:03 -06:00
81ff530a71 Added initial support for direct messages, users need to open conversations for every dm 2022-11-12 11:20:16 -06:00
39fc982665 Added conversations buffer 2022-11-12 09:16:35 -06:00
803f5fbe89 Fixed a few things in tootDialog 2022-11-11 17:32:31 -06:00
0d1d4c887d Added bookmarks buffer 2022-11-11 17:25:04 -06:00
f8f13be6d3 Updated mastodon default config (again) 2022-11-11 17:20:32 -06:00
5f645508ba Added mentions buffer (filters notifications by mention and extract toots from them) 2022-11-11 17:12:03 -06:00
3a3cb3963c Allows to add content warnings when writing a toot 2022-11-11 16:21:15 -06:00
2c298577cc Re-added toot dialog to make it more compatible with mastodon. Added visibility and audio attachments 2022-11-11 15:51:16 -06:00
97815f807a get description only for images, for now 2022-11-11 15:05:56 -06:00
f24f97baae Removed code from our html parser 2022-11-11 15:04:14 -06:00
2a10f029f0 Added basic toot viewing (untested yet) 2022-11-10 17:54:38 -06:00
3deffa57de Added content warnings to templates as $safe_text. Content warnings will be shown by default on GUI for now 2022-11-10 15:32:49 -06:00
d6985be896 Update mastodon default config 2022-11-10 11:57:30 -06:00
0aad2f0ab3 Improved html parsing for toots. Remove Tags from URLList 2022-11-10 10:01:39 -06:00
f151d6554d Added mastodon buffer menus 2022-11-10 07:56:20 -06:00
62d6ae2277 Added favorites buffer, and actions to add, remove and toggle favorite for toots 2022-11-09 17:08:48 -06:00
b405e384c8 Added toot deletion and opening URLS in the browser 2022-11-09 15:52:18 -06:00
7aa986163b Allow to open bosted toots in web browser 2022-11-09 15:23:07 -06:00
59409e61a5 Added playback of audio and video attachments in toots 2022-11-09 13:07:59 -06:00
edbc74262a change character max value for new toot dialog 2022-11-09 12:45:56 -06:00
e07efa46b3 when replying to a conversation, change GUI accordingly and retain original toot's visibility 2022-11-09 11:14:48 -06:00
bedb2e2a4f Hide boost buttons and menu item when focus is in a toot part of a conversation 2022-11-09 11:03:23 -06:00
96b5eec8e0 Added support for creating conversations 2022-11-09 10:59:25 -06:00
c6433d8655 Fixed reply and toot things to make it work properly 2022-11-09 10:20:14 -06:00
c0654658b5 Added indications when there are audio, video, photos or gifvs in media attachments 2022-11-09 09:09:37 -06:00
d71d3695eb Added image descriptions to template system 2022-11-09 08:54:47 -06:00
4b6a7c5d83 Added loading more items in mastodon buffers and enabled buffer buttons for toot, reply and boost 2022-11-09 08:42:56 -06:00
368e089639 Added toot writing and replies 2022-11-08 17:53:59 -06:00
33647da6e8 If user has no display_name set, let's show mastodon username instead 2022-11-08 15:46:16 -06:00
ca39db649b Added sent toots buffer 2022-11-08 15:45:12 -06:00
07dc813cb6 Fixed a small typo 2022-11-08 15:16:01 -06:00
a852a429f4 Implemented boost for toot buffers 2022-11-08 13:49:43 -06:00
1e3a0d9b2e mastodon: Avoid playing sounds on first buffer start 2022-11-08 13:49:15 -06:00
9959ac24d9 Implemented retoots 2022-11-08 13:22:27 -06:00
90f9f18deb added missing settings for mastodon's config 2022-11-08 13:21:45 -06:00
6983d11b73 Added some dialogs for future use 2022-11-08 13:18:04 -06:00
cd5b71a26e fixed small typo 2022-11-08 12:45:40 -06:00
aa3ca82f25 Added buffers on account creation (home, local and federated timelines). Most of functions don't work, just displaying items 2022-11-08 12:41:04 -06:00
bf9b8a4f46 Added mastodon baseBuffer controller 2022-11-08 12:39:04 -06:00
2be460b662 Add templates and buffer order for mastodon default config 2022-11-08 12:21:39 -06:00
1d4057ac5b Added compose function for people in mastodon instances 2022-11-08 12:21:03 -06:00
b9731a3c75 Fixed typos in mastodon buffer GUI 2022-11-08 12:20:11 -06:00
d53decb165 added order_buffer method to sort toots in database 2022-11-08 12:19:41 -06:00
02b8330ca0 Added templates for toots and persons 2022-11-08 12:19:05 -06:00
12046c1f56 Updated compose function for toots 2022-11-08 08:59:09 -06:00
6f69426f03 Fixed more conflicts 2022-11-08 08:48:35 -06:00
c8c242a27f Fixed merge conflicts with next-gen 2022-11-08 08:35:01 -06:00
60daa548ca Added util to parse mastodon toots (very basic, not yet implemented) 2022-11-07 17:13:03 -06:00
40989a54ed cleaned up some imports 2022-11-07 16:15:04 -06:00
87f2976419 moved searches to Twitter handler 2022-11-07 16:06:52 -06:00
b6b81e2b36 Move almost everything related to Twitter to handler 2022-11-07 15:55:28 -06:00
dfcd63b9b6 Moved timelines for users, likes, followers, following and conversations to Twitter handler 2022-11-07 13:55:08 -06:00
23af944fba Moved some interactions with buffers from main controller to the buffer classes themselves 2022-11-07 13:02:54 -06:00
cf9add1fc9 Added empty function view_item to base buffer 2022-11-07 13:01:58 -06:00
a63f19a70f Separate global settings and account settings into two diferent modules so implementing other services will be way easier 2022-11-07 12:27:23 -06:00
8ad266ad1b Moved filters, list features, user actions, user aliases and account config to twitter handler 2022-11-07 12:26:58 -06:00
b593071364 Added empty check_streams to mastodon session so it won't raise errors for a while 2022-11-07 12:24:56 -06:00
0e814a765a Fixed small issue when attempting to apply filters by language 2022-11-07 11:28:42 -06:00
2c7eab60a8 Separate most Twitter features in their own controller so it might be easier to implement those in the handler 2022-11-07 10:25:19 -06:00
d43738a2ec Added base mastodon controller 2022-11-07 09:54:09 -06:00
0f885712b6 Remove unused import in Twitter controller 2022-11-07 09:50:01 -06:00
14a10989e5 Update controller 2022-11-07 09:17:08 -06:00
d5253f651b Merge branch 'next-gen' of gitlab.mcvsoftware.com:twblue/twblue into next-gen 2022-11-07 08:36:58 -06:00
31e901b000 Fixed more locale conflicts 2022-11-06 16:10:56 -06:00
26e7ff009f fixed locales conflicts 2022-11-06 16:07:45 -06:00
1ded773e84 Removed some platform checks 2022-11-03 17:11:13 -06:00
b8637ba8ca Update locales 2022-11-03 11:38:56 -06:00
4dc4f46b84 Removed some platform unused code 2022-11-03 11:34:21 -06:00
e00ec84df3 Updated changelog 2022-11-03 09:52:52 -06:00
8dd122cfb2 Updated ReadMe regarding python version and appkeys 2022-11-03 09:12:41 -06:00
dcfacf8cdd Merge pull request #495 from MCV-Software/python310
Increase Python version to 3.10. Closes #493
2022-11-03 08:53:20 -06:00
bc1522833c Removed appkeys from public repo 2022-11-03 08:47:15 -06:00
4b7382e61f Set python versions to 3.10.8 in CI config File and use a custom WXPython for 32-bit support 2022-11-02 09:42:01 -06:00
585d8e2b0c Removed reference to six and futures as Pytohn 3.10 doesn't need those 2022-11-02 09:27:09 -06:00
f34829d72d Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!9
2022-11-02 15:16:18 +00:00
Steffen Schultz
fd1d60b815 Translated using Weblate (German)
Currently translated at 100.0% (258 of 258 strings)

Translation: TWBlue/changelog
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/changelog/de/
2022-10-12 04:23:53 -05:00
Steffen Schultz
31e194a038 Translated using Weblate (German)
Currently translated at 100.0% (267 of 267 strings)

Translation: TWBlue/documentation
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/documentation/de/
2022-10-05 08:23:54 -05:00
Steffen Schultz
e25712920a Translated using Weblate (German)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/de/
2022-10-05 08:23:53 -05:00
Steffen Schultz
27867a1961 Added translation using Weblate (German) 2022-10-04 07:47:50 -05:00
Steffen Schultz
1425dcd313 Added translation using Weblate (German) 2022-10-04 06:02:54 -05:00
Riku
b9cff1a9c9 Translated using Weblate (Japanese)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ja/
2022-09-09 01:02:02 -05:00
fc1ed54963 Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!8
2022-09-06 18:13:29 +00:00
Jonas S. Marques
442abe82db Translated using Weblate (Portuguese)
Currently translated at 90.5% (721 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/pt/
2022-09-03 19:02:00 -05:00
1b693cd798 Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!7
2022-09-02 22:01:51 +00:00
zvonimir stanecic
8426d8851f Translated using Weblate (Croatian)
Currently translated at 95.6% (761 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/hr/
2022-08-29 12:03:36 -05:00
Corentin Bacqué-Cazenave
eca5aa7bfe Translated using Weblate (French)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2022-08-29 12:03:35 -05:00
485b4c3c6c Code cleanup 2022-03-24 10:39:20 -06:00
b35cdbd7b4 Added handlers to map features from different networks 2022-03-24 10:37:50 -06:00
dbafeb9872 Fixed some typos 2022-02-24 16:29:59 -06:00
c6865a7742 Remove some platform independent code for a while 2022-02-24 16:26:05 -06:00
fcb2ce119b Sorted imports on main controller 2022-02-24 16:18:01 -06:00
9f74d568f0 Add attribute type to mastodon session 2022-02-24 16:08:29 -06:00
e3137f4c3d Merge branch 'unittests' into mastodon 2022-02-24 15:07:11 -06:00
3f58112a61 Finished session manager refactoring 2022-02-24 14:00:07 -06:00
307085b0d5 Merge branch 'next-gen' into mastodon 2022-02-24 13:36:46 -06:00
44ea77b605 Added more docs 2022-02-24 13:35:47 -06:00
085c9038b5 Removed old imports used for python 2 2022-02-24 12:43:06 -06:00
af4e72ee27 Refactored SessionManager so it will use pubsub as a communication pattern between GUI and logic and adds mastodon auth 2022-02-24 12:25:45 -06:00
9322241747 Remove more unneeded code 2022-02-24 10:36:16 -06:00
116a0288d4 Removed code for old issue reporting module 2022-02-24 10:34:11 -06:00
d0322e7131 Merge branch 'next-gen' into mastodon 2022-02-24 10:24:12 -06:00
000cf1aff1 Merged latest code 2022-02-24 08:55:02 -06:00
cf56b518d6 Update Twitter session to Tweepy V4.5.0 2022-02-15 15:56:08 -06:00
1262a9d784 Merge branch 'next-gen' into unittests 2022-01-27 09:11:33 -06:00
e7a30b418a Merge branch 'next-gen' into unittests 2022-01-10 04:48:10 -06:00
8924f787d4 Added unittest for tweet with multiple mentions 2022-01-07 17:44:15 -06:00
b81aff7f8d Fixed syntax error on linux module from keyboard_handler 2022-01-07 17:20:21 -06:00
1dfe256cfe Updated tests for templates 2022-01-07 13:31:15 -06:00
ee8e4b1d8b Added unit for a couple methods on templates 2022-01-07 12:47:34 -06:00
0317eff6a5 Moved current tests from unittest to pytest 2022-01-07 11:41:24 -06:00
793bd2cf5b Added pytest and coverage as dependencies so we can ensure unittests pass later 2022-01-07 08:38:47 -06:00
6b9540a0a8 Added a base mastodon buffer GUI 2021-11-03 13:16:39 -06:00
5687cf6a62 Move Twitter buffers to a specific package 2021-11-03 13:00:35 -06:00
1fd2b5914b Merge branch 'next-gen' into mastodon 2021-11-03 12:31:51 -06:00
d863aafa8c Added mastodon.py to the list of dependencies 2021-08-27 15:47:04 -05:00
5bce84b786 Added default config template for Mastodon 2021-08-27 15:43:20 -05:00
9eab9ad5f0 Added Mastodon authorisation and authentication methods in a session. Added get_user_info, get_lists, get_muted_users 2021-08-27 15:42:34 -05:00
123 changed files with 4725 additions and 16293 deletions

View File

@@ -1,6 +1,6 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
PYTHON: "C:\\python37\\python.exe"
PYTHON: "C:\\python310\\python.exe"
NSIS: "C:\\program files (x86)\\nsis\\makensis.exe"
stages:
@@ -17,9 +17,10 @@ twblue32:
- Set-Variable -Name "time" -Value (date -Format "%H:%m")
- echo ${time}
- echo "started by ${GITLAB_USER_NAME}"
- choco install python --version 3.7.9 -y -ForceX86
- choco install python --version 3.10.8 -y -ForceX86
- '&$env:PYTHON -V'
- '&$env:PYTHON -m pip install --upgrade pip'
- '&$env:PYTHON -m pip install --upgrade https://github.com/josephsl/wxpy32whl/blob/main/wxPython-4.2.0-cp310-cp310-win32.whl?raw=true'
- '&$env:PYTHON -m pip install --upgrade -r requirements.txt'
stage: build
interruptible: true
@@ -56,7 +57,7 @@ twblue64:
- Set-Variable -Name "time" -Value (date -Format "%H:%m")
- echo ${time}
- echo "started by ${GITLAB_USER_NAME}"
- choco install python --version 3.7.9 -y
- choco install python --version 3.10.8 -y
- '&$env:PYTHON -V'
- '&$env:PYTHON -m pip install --upgrade pip'
- '&$env:PYTHON -m pip install --upgrade -r requirements.txt'

View File

@@ -21,6 +21,15 @@ See [TWBlue's webpage](http://twblue.es) for more details.
This document describes how to run tw blue from source and how to build a binary version which doesn't need Python and the other dependencies to run.
### Generating application keys
In order to communicate with Twitter, you will need to generate a set of API keys in their [developer portal](https://developer.twitter.com/en/portal/dashboard) (If you haven't signed up, [visit this site to register as a developer](https://developer.twitter.com/en/docs/twitter-api/getting-started/getting-access-to-the-twitter-api)) and create a module called appkeys.py, within the src directory, with the following content, replacing the example values with your set of API keys:
```
twitter_api_key='xxxxxxxxxx'
twitter_api_secret='xxxxxxxxxx'
```
### Required dependencies.
Although most dependencies can be found in the windows-dependencies directory, we provide links to their official websites. If you are cloning with git, don't forget to initialize and update the submodules to get the windows-dependencies folder. You can use these two commands to perform this task from git bash:
@@ -31,18 +40,22 @@ Although most dependencies can be found in the windows-dependencies directory, w
#### Dependencies packaged in windows installers
* [Python,](https://python.org) version 3.7.9
If you want to build both x86 and x64 binaries, you can install python x86 to C:\python38 and python x64 to C:\python38x64, for example.
* [Python,](https://python.org) version 3.10.8
If you want to build both x86 and x64 binaries, you can install python x64 to C:\python310 and python x86 to C:\python310-32, for example.
#### Dependencies that must be installed using pip
Python installs a tool called Pip that allows to install packages in a simple way. You can find it in the python scripts directory. To install packages using Pip, you have to navigate to the scripts directory using a command prompt, for example:
`cd C:\python37x64\scripts`
`cd C:\python310\scripts`
You can also add the scripts folder to your path environment variable or choose the corresponding option when installing Python.
Note: pip and setuptools are included in the Python installer since version 2.7.9.
Note: If you are using Python for 32-bit systems, you will need to install WXPython for 32-bits before running the command for installing everything else. You can do so by running the following command:
`pip install --upgrade https://github.com/josephsl/wxpy32whl/blob/main/wxPython-4.2.0-cp310-cp310-win32.whl?raw=true`
Pip is able to install packages listed in a special text file, called the requirements file. To install all remaining dependencies, perform the following command:
`pip install -r requirements.txt`

View File

@@ -2,6 +2,39 @@ TWBlue Changelog
## changes in this version
Most of all changes in this release are focused on adding Mastodon support to TWBlue. The features present to handle Twitter should not have been altered in any way. We were not intended to release this version so soon, but unfortunately, Twitter started to present issues in some regions with one particular API endpoint we were using, making impossible for everyone in such regions to use the application. We will release more updates to fix any possible issue regarding Twitter API, but please take into account that this is sometimes an issue happening in Twitter's servers and while we do our best to make TWBlue work despite those problems, you might encounter glitches from time to time.
* TWBlue now builds with Python 3.10.8. ([#493](https://github.com/MCV-Software/TWBlue/issues/493))
* This change also drops support for Windows 7.
* The TWBlue interface has not been translated yet, as we are releasing this update to fix an important Twitter issue for some regions.
* Twitter sessions should be able to be opened properly again in TWBlue, in regions where it didn't work since last week.
* It is now possible to log in to instances of mastodon, hometown and similar software (Pleroma should work as well, although it has not been tested at this time). From the session manager, clicking on the “new account” button will bring up a menu from which you can select whether you want to log in to Twitter or Mastodon. For instances that have a different character limit than the one set by Mastodon, TWBlue will detect the new limit and adjust the dialogs to allow you to use it correctly.
* Most of the TWBlue GUI has been adapted so that the buffers reflect the change of social network (in mastodon, for example, the buttons to write posts say post instead of tweet). However, the menu bar has not yet been updated. This means that most of the options still refer to Twitter, although they can be used with mastodon accounts. For example, if you select the “tweet” menu in the menu bar, and then select the “Retweet” option, TWBlue will actually do a “boost” if the buffer you are in is a Mastodon account buffer.
* Keystrokes for the invisible interface also refer to terms used in Twitter, but can be applied to Mastodon as well.
* There are some features, within TWBlue, that are not yet compatible with mastodon accounts. These are as follows:
* User autocompletion.
* Currently, it is not possible to update account settings for mastodon sessions. However, if you know how to edit configuration files, you can close TWBlue, change your session file with any text editor and restart the application to update what you want.
* The template editor is not yet available for mastodon accounts.
* Filters have not yet been implemented in TWBlue mastodon support.
* User aliases are not implemented yet.
* It is not possible to view a users profile, nor edit your own, for now. However, you can use the keystroke to open the item in the browser when focusing a user to access their profile website. This only works in buffers where users are listed.
* You cannot manage lists in TWBlue at the moment.
* Most of the buffers planned for mastodon should just work. Among those currently tested are: home (main timeline for the logged-in user), Local (public posts for the instance), federated (public posts for all federating instances), mentions, direct messages, sent posts, favorites, bookmarks, followers, following, blocked users, muted users, user searches and timelines for users.
* The difference between favorites and bookmarks is that the author of the post can see who has marked his posts as favorites, but bookmarks are completely private. In any buffer containing mastodon posts, except direct messages, the GUI will display an option to add the post to favorites or bookmarks.
* Direct messages in mastodon are posts, exactly like normal posts, but with their privacy setting set so that they can only be seen by the accounts that are mentioned. In the direct message buffer, a conversation will appear for each item in the buffer. The conversation represents a thread of messages, but TWBlue can only display the last of the messages sent. This is similar to what happens on platforms like Telegram, where you can only see the list of conversations at the beginning. To see the entire thread of direct messages present in a conversation, you can use the command to open the conversation, or go to the “tweet” menu in the menu bar, and then towards the “view conversation” option. This will create a new conversation buffer that will be located just after the direct messages buffer (for the GUI, the buffer will be located just inside the direct messages buffer in the buffer tree). When a private post appears (whose visibility only allows the mentioned accounts to see it), TWBlue will display that post in the home buffer, in mentions and also will update/create the conversation with that item. This is because Mastodon does not differentiate between a private message and a normal post. You can reply to the post in any buffer to continue the conversation. If you reply to any post, the privacy set in the original post is maintained by default, but can also be changed.
* The buffer showing the federated timeline has been disabled from settings. This is because on servers that federate with many instances it can load many posts in a very short time. To enable this buffer, for now, edit the TWBlue configuration while the application is closed, and add the “federated” buffer in the option called “buffer_order”. As soon as buffers can be shown or hidden, this process can be done through the GUI.
* There is a Streaming API that allows the elements for the start buffers, mentions, direct messages, sent posts and followers to appear in real time. This feature is implemented by default and should also just work.
* Timelines for users only allow to get all posts from users who are in the same instance. For users belonging to other instances, you can get the posts that have been downloaded to your instance since your instance “knows” the remote user.
* Timelines for followers and following can be fully retrieved only for users belonging to the same instance. Remote users may yield unclear results.
* You can search by users (by opening a search and selecting the “users” radio button). The search can be done by local users, such as twblue, or by remote users, such as @twblue@maaw.social.
* In all buffers, a maximum of 40 items are retrieved per load, but more can be retrieved by using the option to load more items in the buffer.
* In post buffers, you can do most of the actions already supported in TWBlue (boost, add/remove from favorites or bookmarks, reply, send message to user, open post URL, play audio or video, open post on web, view conversation, open action dialog for user).
* In user buffers, you can send private message to the user, and open user actions dialog, which in turn allows you to follow/unfollow, block/unblock and mute/unmute.
* When writing posts, it is possible to attach up to 4 images, 4 givs, or even a video, poll, or audio. It is also possible to add the “sensitive content” tag to posts, change privacy and write a content warning text. It is possible to create threads using the “add post” button.
* When replying to a post, TWBlue will place the username of all participants in the item you reply to. The privacy options will default to those of the original post.
## Changes in version 2022.8.28
* the user autocompletion feature has been completely rewritten to be easier to use, particularly for people with many followers/following users:
* In the account settings dialog, there's a button that opens up a new dialog that allows you to "scan" your account in order to add all users from your followers/following list. This process will read your data directly from Twitter and depending in the amount of people you have in your account it might take too many API calls. Please use it with caution. You can, for example, do the process separately for your followers/following people so it will be easier to handle, in case you have a massive amount of people. If TWBlue is unable to complete the scan, you will see an error and will be prompted to try again in 15 minutes, once your API calls have refreshed.
* It is possible to use the user autocompletion functionality in dialogs where you can select an user, for example when adding or removing someone from a list, or displaying lists for someone.

View File

@@ -1,9 +1,10 @@
wxpython==4.1.1
wxpython
pytest
coverage
wheel
six
future
configobj
markdown
future
requests
oauthlib
requests-oauthlib
@@ -12,7 +13,6 @@ pypubsub
geopy
arrow
python-dateutil
futures
winpaths
PySocks
win_inet_pton
@@ -31,6 +31,7 @@ backports.functools_lru_cache
cx_freeze
tweepy
twitter-text-parser
mastodon.py
pyenchant
sqlitedict
cx-Logging

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import base as base
from . import twitter as twitter
from . import twitter as twitter
from . import mastodon as mastodon

View File

@@ -138,4 +138,7 @@ class Buffer(object):
try:
self.session.db[self.name+"_pos"]=self.buffer.list.get_selected()
except AttributeError:
pass
pass
def view_item(self):
pass

View File

@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
from .base import BaseBuffer
from .base import BaseBuffer
from .mentions import MentionsBuffer
from .conversations import ConversationBuffer, ConversationListBuffer
from .users import UserBuffer

View File

@@ -1,141 +1,498 @@
# -*- coding: utf-8 -*-
""" Common logic to all buffers in TWBlue."""
import logging
# -*- coding: utf-8 -*-
import time
import wx
import output
import sound
import widgetUtils
import arrow
import webbrowser
import output
import config
import sound
import languageHandler
import logging
from audio_services import youtube_utils
from controller.buffers.base import base
from controller.mastodon import messages
from sessions.mastodon import compose, utils, templates
from mysc.thread_utils import call_threaded
from pubsub import pub
from extra import ocr
from wxUI import buffers, dialogs, commonMessageDialogs
from wxUI.dialogs.mastodon import menus
from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs
log = logging.getLogger("controller.buffers.base.base")
log = logging.getLogger("controller.buffers.mastodon.base")
class Buffer(object):
""" A basic buffer object. This should be the base class for all other derived buffers."""
class BaseBuffer(base.Buffer):
def __init__(self, parent, function, name, sessionObject, account, sound=None, compose_func="compose_post", *args, **kwargs):
super(BaseBuffer, self).__init__(parent, function, *args, **kwargs)
log.debug("Initializing buffer %s, account %s" % (name, account,))
self.create_buffer(parent, name)
self.invisible = True
self.name = name
self.type = self.buffer.type
self.session = sessionObject
self.compose_function = getattr(compose, compose_func)
log.debug("Compose_function: %s" % (self.compose_function,))
self.account = account
self.buffer.account = account
self.bind_events()
self.sound = sound
if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name:
self.finished_timeline = False
def __init__(self, parent=None, function=None, session=None, *args, **kwargs):
"""Inits the main controller for this buffer:
@ parent wx.Treebook object: Container where we will put this buffer.
@ function str or None: function to be called periodically and update items on this buffer.
@ session sessionmanager.session object or None: Session handler for settings, database and data access.
"""
super(Buffer, self).__init__()
self.function = function
# Compose_function will be used to render an object on this buffer. Normally, signature is as follows:
# compose_function(item, db, relative_times, show_screen_names=False, session=None)
# Read more about compose functions in sessions/twitter/compose.py.
self.compose_function = None
self.args = args
self.kwargs = kwargs
# This will be used as a reference to the wx.Panel object wich stores the buffer GUI.
self.buffer = None
# This should countains the account associated to this buffer.
self.account = ""
# This controls whether the start_stream function should be called when starting the program.
self.needs_init = True
# if this is set to False, the buffer will be ignored on the invisible interface.
self.invisible = False
# Control variable, used to track time of execution for calls to start_stream.
self.execution_time = 0
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.basePanel(parent, name)
def clear_list(self):
pass
def get_event(self, ev):
""" Catch key presses in the WX interface and generate the corresponding event names."""
if ev.GetKeyCode() == wx.WXK_RETURN and ev.ControlDown(): event = "audio"
elif ev.GetKeyCode() == wx.WXK_RETURN: event = "url"
elif ev.GetKeyCode() == wx.WXK_F5: event = "volume_down"
elif ev.GetKeyCode() == wx.WXK_F6: event = "volume_up"
elif ev.GetKeyCode() == wx.WXK_DELETE and ev.ShiftDown(): event = "clear_list"
elif ev.GetKeyCode() == wx.WXK_DELETE: event = "destroy_status"
# Raise a Special event when pressed Shift+F10 because Wx==4.1.x does not seems to trigger this by itself.
# See https://github.com/manuelcortez/TWBlue/issues/353
elif ev.GetKeyCode() == wx.WXK_F10 and ev.ShiftDown(): event = "show_menu"
else:
event = None
ev.Skip()
if event != None:
try:
### ToDo: Remove after WX fixes issue #353 in the widgets.
if event == "show_menu":
return self.show_menu(widgetUtils.MENU, pos=self.buffer.list.list.GetPosition())
getattr(self, event)()
except AttributeError:
pass
def volume_down(self):
""" Decreases volume by 5%"""
if self.session.settings["sound"]["volume"] > 0.0:
if self.session.settings["sound"]["volume"] <= 0.05:
self.session.settings["sound"]["volume"] = 0.0
else:
self.session.settings["sound"]["volume"] -=0.05
sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100.0))
self.session.sound.play("volume_changed.ogg")
self.session.settings.write()
def volume_up(self):
""" Increases volume by 5%."""
if self.session.settings["sound"]["volume"] < 1.0:
if self.session.settings["sound"]["volume"] >= 0.95:
self.session.settings["sound"]["volume"] = 1.0
else:
self.session.settings["sound"]["volume"] +=0.05
sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100))
self.session.sound.play("volume_changed.ogg")
self.session.settings.write()
def start_stream(self, mandatory=False, play_sound=True):
pass
def get_more_items(self):
output.speak(_(u"This action is not supported for this buffer"), True)
def put_items_on_list(self, items):
pass
def remove_buffer(self):
return False
def remove_item(self, item):
f = self.buffer.list.get_selected()
self.buffer.list.remove_item(item)
self.buffer.list.select_item(f)
def bind_events(self):
pass
def get_object(self):
return self.buffer
def get_message(self):
pass
def set_list_position(self, reversed=False):
if reversed == False:
self.buffer.list.select_item(-1)
else:
self.buffer.list.select_item(0)
def reply(self):
pass
def send_message(self):
pass
def share_item(self):
pass
def can_share(self):
pass
def destroy_status(self):
pass
def get_buffer_name(self):
""" Get buffer name from a set of different techniques."""
# firstly let's take the easier buffers.
basic_buffers = dict(home_timeline=_("Home"), local_timeline=_("Local"), federated_timeline=_("Federated"), mentions=_("Mentions"), bookmarks=_("Bookmarks"), direct_messages=_("Direct messages"), sent=_("Sent"), favorites=_("Favorites"), followers=_("Followers"), following=_("Following"), blocked=_("Blocked users"), muted=_("Muted users"), notifications=_("Notifications"))
if self.name in list(basic_buffers.keys()):
return basic_buffers[self.name]
# Check user timelines
elif hasattr(self, "username"):
if "-timeline" in self.name:
return _(u"{username}'s timeline").format(username=self.username,)
elif "-followers" in self.name:
return _(u"{username}'s followers").format(username=self.username,)
elif "-following" in self.name:
return _(u"{username}'s following").format(username=self.username,)
log.error("Error getting name for buffer %s" % (self.name,))
return _(u"Unknown buffer")
def post_status(self, *args, **kwargs):
title = _("Post")
caption = _("Write your post here")
post = messages.post(session=self.session, title=title, caption=caption)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility=post.get_visibility(), **kwargs)
if hasattr(post.message, "destroy"):
post.message.destroy()
def get_formatted_message(self):
return self.compose_function(self.get_item(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])[1]
def get_message(self):
post = self.get_item()
if post == None:
return
template = self.session.settings["templates"]["post"]
t = templates.render_post(post, template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
# toDo: Implement reverse timelines properly here.
if (self.name != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
min_id = self.session.db[self.name][-1].id
try:
results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
if hasattr(self, "finished_timeline") and self.finished_timeline == False:
if "-timeline" in self.name:
self.username = self.session.db[self.name][0]["account"].username
self.finished_timeline = True
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def auto_read(self, number_of_items):
if number_of_items == 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
if self.session.settings["general"]["reverse_timelines"] == False:
post = self.session.db[self.name][-1]
else:
post = self.session.db[self.name][0]
output.speak(_("New post in {0}").format(self.get_buffer_name()))
output.speak(" ".join(self.compose_function(post, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])))
elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
output.speak(_("{0} new posts in {1}.").format(number_of_items, self.get_buffer_name()))
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
else:
max_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def remove_buffer(self, force=False):
if "-timeline" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.kwargs.get("id") in self.session.settings["other_buffers"]["timelines"]:
self.session.settings["other_buffers"]["timelines"].remove(self.kwargs.get("id"))
self.session.settings.write()
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
else:
output.speak(_(u"This buffer is not a timeline; it can't be deleted."), True)
return False
def put_items_on_list(self, number_of_items):
list_to_use = self.session.db[self.name]
if number_of_items == 0 and self.session.settings["general"]["persist_size"] == 0: return
log.debug("The list contains %d items " % (self.buffer.list.get_count(),))
log.debug("Putting %d items on the list" % (number_of_items,))
if self.buffer.list.get_count() == 0:
for i in list_to_use:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.set_position(self.session.settings["general"]["reverse_timelines"])
elif self.buffer.list.get_count() > 0 and number_of_items > 0:
if self.session.settings["general"]["reverse_timelines"] == False:
items = list_to_use[len(list_to_use)-number_of_items:]
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
else:
items = list_to_use[0:number_of_items]
items.reverse()
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
log.debug("Now the list contains %d items " % (self.buffer.list.get_count(),))
def add_new_item(self, item):
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
if self.session.settings["general"]["reverse_timelines"] == False:
self.buffer.list.insert_item(False, *post)
else:
self.buffer.list.insert_item(True, *post)
if self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
output.speak(" ".join(post[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"])
def update_item(self, item, position):
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.list.SetItem(position, 1, post[1])
def bind_events(self):
log.debug("Binding events...")
self.buffer.set_focus_function(self.onFocus)
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.share_item, self.buffer.boost)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.send_message, self.buffer.dm)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.toggle_favorite, self.buffer.fav)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.toggle_bookmark, self.buffer.bookmark)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
def show_menu(self, ev, pos=0, *args, **kwargs):
if self.buffer.list.get_count() == 0:
return
menu = menus.base()
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.audio, menuitem=menu.play)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.copy, menuitem=menu.copy)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.destroy_status, menuitem=menu.remove)
if hasattr(menu, "openInBrowser"):
widgetUtils.connect_event(menu, widgetUtils.MENU, self.open_in_browser, menuitem=menu.openInBrowser)
if pos != 0:
self.buffer.PopupMenu(menu, pos)
else:
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())
def view(self, *args, **kwargs):
pub.sendMessage("execute-action", action="view_item")
def copy(self, *args, **kwargs):
pub.sendMessage("execute-action", action="copy_to_clipboard")
def user_actions(self, *args, **kwargs):
pub.sendMessage("execute-action", action="follow")
def fav(self, *args, **kwargs):
pub.sendMessage("execute-action", action="add_to_favourites")
def unfav(self, *args, **kwargs):
pub.sendMessage("execute-action", action="remove_from_favourites")
def delete_item_(self, *args, **kwargs):
pub.sendMessage("execute-action", action="delete_item")
def url_(self, *args, **kwargs):
self.url()
def show_menu_by_key(self, ev):
if self.buffer.list.get_count() == 0:
return
if ev.GetKeyCode() == wx.WXK_WINDOWS_MENU:
self.show_menu(widgetUtils.MENU, pos=self.buffer.list.list.GetPosition())
def get_item(self):
index = self.buffer.list.get_selected()
if index > -1 and self.session.db.get(self.name) != None:
return self.session.db[self.name][index]
def can_share(self):
post = self.get_item()
if post.visibility == "direct":
return False
return True
def reply(self, *args):
item = self.get_item()
visibility = item.visibility
if visibility == "direct":
title = _("Conversation with {}").format(item.account.username)
caption = _("Write your message here")
else:
title = _("Reply to {}").format(item.account.username)
caption = _("Write your reply here")
if item.reblog != None:
users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]]
if item.reblog.account.acct != item.account.acct and "@{} ".format(item.reblog.account.acct) not in users:
users.append("@{} ".format(item.reblog.account.acct))
else:
users = ["@{} ".format(user.acct) for user in item.mentions if user.id != self.session.db["user_id"]]
if "@{} ".format(item.account.acct) not in users and item.account.id != self.session.db["user_id"]:
users.insert(0, "@{} ".format(item.account.acct))
users_str = "".join(users)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
post.message.visibility.SetSelection(visibility_settings.get(visibility))
# Respect content warning settings.
if item.sensitive:
post.message.sensitive.SetValue(item.sensitive)
post.message.spoiler.ChangeValue(item.spoiler_text)
post.message.on_sensitivity_changed()
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=post.get_visibility())
if hasattr(post.message, "destroy"):
post.message.destroy()
def send_message(self, *args, **kwargs):
item = self.get_item()
title = _("Conversation with {}").format(item.account.username)
caption = _("Write your message here")
if item.reblog != None:
users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]]
if item.reblog.account.acct != item.account.acct and "@{} ".format(item.reblog.account.acct) not in users:
users.append("@{} ".format(item.reblog.account.acct))
else:
users = ["@{} ".format(user.acct) for user in item.mentions if user.id != self.session.db["user_id"]]
if item.account.acct not in users and item.account.id != self.session.db["user_id"]:
users.insert(0, "@{} ".format(item.account.acct))
users_str = "".join(users)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
post.message.visibility.SetSelection(3)
if item.sensitive:
post.message.sensitive.SetValue(item.sensitive)
post.message.spoiler.ChangeValue(item.spoiler_text)
post.message.on_sensitivity_changed()
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct", reply_to=item.id)
if hasattr(post.message, "destroy"):
post.message.destroy()
def share_item(self, *args, **kwargs):
if self.can_share() == False:
return output.speak(_("This action is not supported on conversation posts."))
post = self.get_item()
id = post.id
if self.session.settings["general"]["boost_mode"] == "ask":
answer = mastodon_dialogs.boost_question()
if answer == True:
self._direct_boost(id)
else:
self._direct_boost(id)
def _direct_boost(self, id):
item = self.session.api_call(call_name="status_reblog", _sound="retweet_send.ogg", id=id)
def onFocus(self, *args, **kwargs):
post = self.get_item()
if self.session.settings["general"]["relative_times"] == True:
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at)
ts = original_date.humanize(locale=languageHandler.getLanguage())
self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 2, ts)
if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post):
self.session.sound.play("audio.ogg")
if self.session.settings['sound']['indicate_img'] and utils.is_image(post):
self.session.sound.play("image.ogg")
can_share = self.can_share()
pub.sendMessage("toggleShare", shareable=can_share)
self.buffer.boost.Enable(can_share)
def audio(self, url='', *args, **kwargs):
if sound.URLPlayer.player.is_playing():
return sound.URLPlayer.stop_audio()
item = self.get_item()
if item == None:
return
urls = utils.get_media_urls(item)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = dialogs.urlList.urlList()
urls_list.populate_list(urls)
if urls_list.get_response() == widgetUtils.OK:
url=urls_list.get_string()
if hasattr(urls_list, "destroy"): urls_list.destroy()
if url != '':
# try:
sound.URLPlayer.play(url, self.session.settings["sound"]["volume"])
# except:
# log.error("Exception while executing audio method.")
def url(self, url='', announce=True, *args, **kwargs):
if url == '':
post = self.get_item()
if post.reblog != None:
urls = utils.find_urls(post.reblog)
else:
urls = utils.find_urls(post)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = dialogs.urlList.urlList()
urls_list.populate_list(urls)
if urls_list.get_response() == widgetUtils.OK:
url=urls_list.get_string()
if hasattr(urls_list, "destroy"): urls_list.destroy()
if url != '':
if announce:
output.speak(_(u"Opening URL..."), True)
webbrowser.open_new_tab(url)
def clear_list(self):
dlg = commonMessageDialogs.clear_list()
if dlg == widgetUtils.YES:
self.session.db[self.name] = []
self.buffer.list.clear()
def destroy_status(self, *args, **kwargs):
index = self.buffer.list.get_selected()
item = self.session.db[self.name][index]
if item.account.id != self.session.db["user_id"] or item.reblog != None:
output.speak(_("You can delete only your own posts."))
return
answer = mastodon_dialogs.delete_post_dialog()
if answer == True:
items = self.session.db[self.name]
try:
self.session.api.status_delete(id=item.id)
items.pop(index)
self.buffer.list.remove_item(index)
except Exception as e:
self.session.sound.play("error.ogg")
self.session.db[self.name] = items
def user_details(self):
item = self.get_item()
pass
def save_positions(self):
try:
self.session.db[self.name+"_pos"]=self.buffer.list.get_selected()
except AttributeError:
pass
def get_item_url(self):
post = self.get_item()
if post.reblog != None:
return post.reblog.url
return post.url
def open_in_browser(self, *args, **kwargs):
url = self.get_item_url()
output.speak(_("Opening item in web browser..."))
webbrowser.open(url)
def add_to_favorites(self):
item = self.get_item()
if item.reblog != None:
item = item.reblog
call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id)
def remove_from_favorites(self):
item = self.get_item()
if item.reblog != None:
item = item.reblog
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_favorite(self, *args, **kwargs):
item = self.get_item()
if item.reblog != None:
item = item.reblog
item = self.session.api.status(item.id)
if item.favourited == False:
call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id)
else:
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_bookmark(self, *args, **kwargs):
item = self.get_item()
if item.reblog != None:
item = item.reblog
item = self.session.api.status(item.id)
if item.bookmarked == False:
call_threaded(self.session.api_call, call_name="status_bookmark", preexec_message=_("Adding to bookmarks..."), _sound="favourite.ogg", id=item.id)
else:
call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id)
def view_item(self):
post = self.get_item()
# Update object so we can retrieve newer stats
post = self.session.api.status(id=post.id)
print(post)
msg = messages.viewPost(post, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url())
def ocr_image(self):
post = self.get_item()
media_list = []
pass

View File

@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-
import time
import logging
import wx
import widgetUtils
import output
from controller.mastodon import messages
from controller.buffers.mastodon.base import BaseBuffer
from mysc.thread_utils import call_threaded
from sessions.mastodon import utils, templates
from wxUI import buffers, commonMessageDialogs
log = logging.getLogger("controller.buffers.mastodon.conversations")
class ConversationListBuffer(BaseBuffer):
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.conversationListPanel(parent, name)
def get_item(self):
index = self.buffer.list.get_selected()
if index > -1 and self.session.db.get(self.name) != None and len(self.session.db[self.name]) > index:
return self.session.db[self.name][index]["last_status"]
def get_conversation(self):
index = self.buffer.list.get_selected()
if index > -1 and self.session.db.get(self.name) != None:
return self.session.db[self.name][index]
def get_formatted_message(self):
return self.compose_function(self.get_conversation(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])[1]
def get_message(self):
conversation = self.get_conversation()
if conversation == None:
return
template = self.session.settings["templates"]["conversation"]
post_template = self.session.settings["templates"]["post"]
t = templates.render_conversation(conversation=conversation, template=template, post_template=post_template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
# toDo: Implement reverse timelines properly here.
# if (self.name != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
# min_id = self.session.db[self.name][-1].id
try:
results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
new_position, number_of_items = self.order_buffer(results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if new_position > -1:
self.buffer.list.select_item(new_position)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].last_status.id
else:
max_id = self.session.db[self.name][-1].last_status.id
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
conversation = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *conversation)
else:
for i in items:
conversation = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *conversation)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def get_item_position(self, conversation):
for i in range(len(self.session.db[self.name])):
if self.session.db[self.name][i].id == conversation.id:
return i
def order_buffer(self, data):
num = 0
focus_object = None
if self.session.db.get(self.name) == None:
self.session.db[self.name] = []
objects = self.session.db[self.name]
for i in data:
position = self.get_item_position(i)
if position != None:
conversation = self.session.db[self.name][position]
if conversation.last_status.id != i.last_status.id:
focus_object = i
objects.pop(position)
self.buffer.list.remove_item(position)
if self.session.settings["general"]["reverse_timelines"] == False:
objects.append(i)
else:
objects.insert(0, i)
num = num+1
else:
if self.session.settings["general"]["reverse_timelines"] == False:
objects.append(i)
else:
objects.insert(0, i)
num = num+1
self.session.db[self.name] = objects
if focus_object == None:
return (-1, num)
new_position = self.get_item_position(focus_object)
if new_position != None:
return (new_position, num)
return (-1, num)
def bind_events(self):
log.debug("Binding events...")
self.buffer.set_focus_function(self.onFocus)
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
def fav(self):
pass
def unfav(self):
pass
def can_share(self):
return False
def send_message(self):
return self.reply()
def onFocus(self, *args, **kwargs):
post = self.get_item()
if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post):
self.session.sound.play("audio.ogg")
if self.session.settings['sound']['indicate_img'] and utils.is_image(post):
self.session.sound.play("image.ogg")
def destroy_status(self):
pass
def reply(self, *args):
item = self.get_item()
conversation = self.get_conversation()
visibility = item.visibility
title = _("Reply to conversation with {}").format(conversation.accounts[0].username)
caption = _("Write your message here")
users = ["@{} ".format(user.acct) for user in conversation.accounts]
users_str = "".join(users)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
post.message.visibility.SetSelection(visibility_settings.get(visibility))
if item.sensitive:
post.message.sensitive.SetValue(item.sensitive)
post.message.spoiler.ChangeValue(item.spoiler_text)
post.message.on_sensitivity_changed()
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=visibility)
if hasattr(post.message, "destroy"):
post.message.destroy()
class ConversationBuffer(BaseBuffer):
def __init__(self, post, *args, **kwargs):
self.post = post
super(ConversationBuffer, self).__init__(*args, **kwargs)
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
self.post = self.session.api.status(id=self.post.id)
# toDo: Implement reverse timelines properly here.
try:
results = []
items = getattr(self.session.api, self.function)(*self.args, **self.kwargs)
[results.append(item) for item in items.ancestors]
results.append(self.post)
[results.append(item) for item in items.descendants]
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
output.speak(_(u"This action is not supported for this buffer"), True)
def remove_buffer(self, force=False):
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
import time
import logging
from controller.buffers.mastodon.base import BaseBuffer
from sessions.mastodon import utils
log = logging.getLogger("controller.buffers.mastodon.mentions")
class MentionsBuffer(BaseBuffer):
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
# toDo: Implement reverse timelines properly here.
# if self.name != "favorites" and self.name in self.session.db and len(self.session.db[self.name]) > 0:
# min_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(min_id=min_id, limit=count, exclude_types=["follow", "favourite", "reblog", "poll", "follow_request"], *self.args, **self.kwargs)
items.reverse()
results = [item.status for item in items if item.get("status") and item.type == "mention"]
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
else:
max_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], exclude_types=["follow", "favourite", "reblog", "poll", "follow_request"], *self.args, **self.kwargs)
items = [item.status for item in items if item.get("status") and item.type == "mention"]
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)

View File

@@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
import time
import logging
import wx
import widgetUtils
import output
from mysc.thread_utils import call_threaded
from controller.buffers.mastodon.base import BaseBuffer
from controller.mastodon import messages
from sessions.mastodon import templates, utils
from wxUI import buffers, commonMessageDialogs
log = logging.getLogger("controller.buffers.mastodon.conversations")
class UserBuffer(BaseBuffer):
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.userPanel(parent, name)
def get_message(self):
user = self.get_item()
if user == None:
return
template = self.session.settings["templates"]["person"]
t = templates.render_user(user=user, template=template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def bind_events(self):
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.send_message, self.buffer.message)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.user_actions, self.buffer.actions)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
def fav(self):
pass
def unfav(self):
pass
def can_share(self):
return False
def reply(self, *args, **kwargs):
return self.send_message()
def send_message(self, *args, **kwargs):
item = self.get_item()
title = _("New conversation with {}").format(item.username)
caption = _("Write your message here")
users_str = "@{} ".format(item.acct)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
post.message.visibility.SetSelection(3)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct")
if hasattr(post.message, "destroy"):
post.message.destroy()
def audio(self):
pass
def url(self):
pass
def destroy_status(self):
pass
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
# toDo: Implement reverse timelines properly here.
try:
results = getattr(self.session.api, self.function)(limit=count, *self.args, **self.kwargs)
if hasattr(results, "_pagination_next") and self.name not in self.session.db["pagination_info"]:
self.session.db["pagination_info"][self.name] = results._pagination_next
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
if hasattr(self, "finished_timeline") and self.finished_timeline == False:
if "-followers" in self.name or "-following" in self.name:
self.username = self.session.api.account(id=self.kwargs.get("id")).username
self.finished_timeline = True
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
elements = []
pagination_info = self.session.db["pagination_info"].get(self.name)
if pagination_info == None:
output.speak(_("There are no more items in this buffer."))
return
try:
items = self.session.api.fetch_next(pagination_info)
if hasattr(items, "_pagination_next"):
self.session.db["pagination_info"][self.name] = items._pagination_next
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def get_item_url(self):
item = self.get_item()
return item.url
def user_details(self):
item = self.get_item()
pass
def add_to_favorites(self):
pass
def remove_from_favorites(self):
pass
def toggle_favorite(self):
pass
def view_item(self):
item = self.get_item()
print(item)
def ocr_image(self):
pass
def remove_buffer(self, force=False):
if "-followers" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.kwargs.get("id") in self.session.settings["other_buffers"]["followers_timelines"]:
self.session.settings["other_buffers"]["followers_timelines"].remove(self.kwargs.get("id"))
self.session.settings.write()
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
elif "-following" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.kwargs.get("id") in self.session.settings["other_buffers"]["following_timelines"]:
self.session.settings["other_buffers"]["following_timelines"].remove(self.kwargs.get("id"))
self.session.settings.write()
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
elif "-searchUser" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
else:
output.speak(_(u"This buffer is not a timeline; it can't be deleted."), True)
return False

View File

@@ -1,14 +1,6 @@
# -*- coding: utf-8 -*-
import time
import platform
if platform.system() == "Windows":
import wx
from wxUI import buffers, dialogs, commonMessageDialogs, menus
from controller import user
elif platform.system() == "Linux":
from gi.repository import Gtk
from gtkUI import buffers, dialogs, commonMessageDialogs
from controller import messages
import wx
import widgetUtils
import arrow
import webbrowser
@@ -24,7 +16,10 @@ from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException
from tweepy.cursor import Cursor
from pubsub import pub
from extra import ocr
from sessions.twitter.long_tweets import twishort, tweets
from wxUI import buffers, dialogs, commonMessageDialogs, menus
from controller.twitter import user, messages
log = logging.getLogger("controller.buffers")
@@ -40,9 +35,9 @@ class BaseBuffer(base.Buffer):
super(BaseBuffer, self).__init__(parent, function, *args, **kwargs)
log.debug("Initializing buffer %s, account %s" % (name, account,))
if bufferType != None:
self.buffer = getattr(buffers, bufferType)(parent, name)
self.buffer = getattr(buffers.twitter, bufferType)(parent, name)
else:
self.buffer = buffers.basePanel(parent, name)
self.buffer = buffers.twitter.basePanel(parent, name)
self.invisible = True
self.name = name
self.type = self.buffer.type
@@ -309,9 +304,6 @@ class BaseBuffer(base.Buffer):
self.buffer.list.insert_item(True, *tweet)
if self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
output.speak(" ".join(tweet[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"])
#Improve performance on Windows
# if platform.system() == "Windows":
# call_threaded(utils.is_audio,item)
def bind_events(self):
log.debug("Binding events...")
@@ -322,7 +314,6 @@ class BaseBuffer(base.Buffer):
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.share_item, self.buffer.retweet)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.send_message, self.buffer.dm)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply)
# Replace for the correct way in other platforms.
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
@@ -480,7 +471,7 @@ class BaseBuffer(base.Buffer):
def onFocus(self, *args, **kwargs):
tweet = self.get_tweet()
if platform.system() == "Windows" and self.session.settings["general"]["relative_times"] == True:
if self.session.settings["general"]["relative_times"] == True:
# fix this:
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at, locale="en")
ts = original_date.humanize(locale=languageHandler.getLanguage())
@@ -589,4 +580,84 @@ class BaseBuffer(base.Buffer):
def open_in_browser(self, *args, **kwargs):
url = self.get_item_url()
output.speak(_(u"Opening item in web browser..."))
webbrowser.open(url)
webbrowser.open(url)
def add_to_favorites(self):
id = self.get_tweet().id
call_threaded(self.session.api_call, call_name="create_favorite", _sound="favourite.ogg", id=id)
def remove_from_favorites(self):
id = self.get_tweet().id
call_threaded(self.session.api_call, call_name="destroy_favorite", id=id)
def toggle_favorite(self):
id = self.get_tweet().id
tweet = self.session.twitter.get_status(id=id, include_ext_alt_text=True, tweet_mode="extended")
if tweet.favorited == False:
call_threaded(self.session.api_call, call_name="create_favorite", _sound="favourite.ogg", id=id)
else:
call_threaded(self.session.api_call, call_name="destroy_favorite", id=id)
def view_item(self):
if self.type == "dm" or self.name == "direct_messages":
non_tweet = self.get_formatted_message()
item = self.get_right_tweet()
original_date = arrow.get(int(item.created_timestamp))
date = original_date.shift(seconds=self.session.db["utc_offset"]).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage())
msg = messages.viewTweet(non_tweet, [], False, date=date)
else:
tweet, tweetsList = self.get_full_tweet()
msg = messages.viewTweet(tweet, tweetsList, utc_offset=self.session.db["utc_offset"], item_url=self.get_item_url())
def reverse_geocode(self, geocoder):
try:
tweet = self.get_tweet()
if tweet.coordinates != None:
x = tweet.coordinates["coordinates"][0]
y = tweet.coordinates["coordinates"][1]
address = geocoder.reverse_geocode(y, x, language = languageHandler.curLang)
return address
else:
output.speak(_("There are no coordinates in this tweet"))
# except GeocoderError:
# output.speak(_(u"There are no results for the coordinates in this tweet"))
except ValueError:
output.speak(_(u"Error decoding coordinates. Try again later."))
except KeyError:
pass
except AttributeError:
pass
def ocr_image(self):
tweet = self.get_tweet()
media_list = []
if hasattr(tweet, "entities") and tweet.entities.get("media") != None:
[media_list.append(i) for i in tweet.entities["media"] if i not in media_list]
elif hasattr(tweet, "retweeted_status") and tweet.retweeted_status.get("media") != None:
[media_list.append(i) for i in tweet.retweeted_status.entities["media"] if i not in media_list]
elif hasattr(tweet, "quoted_status") and tweet.quoted_status.entities.get("media") != None:
[media_list.append(i) for i in tweet.quoted_status.entities["media"] if i not in media_list]
if len(media_list) > 1:
image_list = [_(u"Picture {0}").format(i,) for i in range(0, len(media_list))]
dialog = dialogs.urlList.urlList(title=_(u"Select the picture"))
if dialog.get_response() == widgetUtils.OK:
img = media_list[dialog.get_item()]
else:
return
elif len(media_list) == 1:
img = media_list[0]
else:
output.speak(_(u"Invalid buffer"))
return
if self.session.settings["mysc"]["ocr_language"] != "":
ocr_lang = self.session.settings["mysc"]["ocr_language"]
else:
ocr_lang = ocr.OCRSpace.short_langs.index(tweet.lang)
ocr_lang = ocr.OCRSpace.OcrLangs[ocr_lang]
api = ocr.OCRSpace.OCRSpaceAPI()
try:
text = api.OCR_URL(img["media_url"], lang=ocr_lang)
except ocr.OCRSpace.APIError as er:
output.speak(_(u"Unable to extract text"))
return
msg = messages.viewTweet(text["ParsedText"], [], False)

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
import platform
import widgetUtils
import arrow
import webbrowser
@@ -7,12 +6,12 @@ import output
import config
import languageHandler
import logging
from controller import messages
from sessions.twitter import compose, utils, templates
from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException
from pubsub import pub
from wxUI import commonMessageDialogs
from controller.twitter import messages
from . import base
log = logging.getLogger("controller.buffers.twitter.dmBuffer")
@@ -70,7 +69,7 @@ class DirectMessagesBuffer(base.BaseBuffer):
self.session.db["sent_direct_messages"] = sent_dms
user_ids = [item.message_create["sender_id"] for item in items]
self.session.save_users(user_ids)
pub.sendMessage("more-sent-dms", data=sent, account=self.session.db["user_name"])
pub.sendMessage("twitter.more_sent_dms", data=sent, account=self.session.db["user_name"])
selected = self.buffer.list.get_selected()
if self.session.settings["general"]["reverse_timelines"] == True:
for i in received:
@@ -99,7 +98,7 @@ class DirectMessagesBuffer(base.BaseBuffer):
def onFocus(self, *args, **kwargs):
tweet = self.get_tweet()
if platform.system() == "Windows" and self.session.settings["general"]["relative_times"] == True:
if self.session.settings["general"]["relative_times"] == True:
# fix this:
original_date = arrow.get(int(tweet.created_timestamp))
ts = original_date.humanize(locale=languageHandler.getLanguage())
@@ -163,3 +162,10 @@ class SentDirectMessagesBuffer(DirectMessagesBuffer):
dm = self.get_right_tweet()
t = templates.render_dm(dm, template, self.session, relative_times=self.session.settings["general"]["relative_times"], offset_seconds=self.session.db["utc_offset"])
return t
def view_item(self):
non_tweet = self.get_formatted_message()
item = self.get_right_tweet()
original_date = arrow.get(int(item.created_timestamp))
date = original_date.shift(seconds=self.session.db["utc_offset"]).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage())
msg = messages.viewTweet(non_tweet, [], False, date=date)

View File

@@ -1,13 +1,8 @@
# -*- coding: utf-8 -*-
import platform
if platform.system() == "Windows":
from wxUI import dialogs, commonMessageDialogs
elif platform.system() == "Linux":
from gi.repository import Gtk
from gtkUI import dialogs, commonMessageDialogs
import widgetUtils
import logging
from tweepy.cursor import Cursor
from wxUI import dialogs, commonMessageDialogs
from . import base
log = logging.getLogger("controller.buffers.twitter.listBuffer")

View File

@@ -1,13 +1,5 @@
# -*- coding: utf-8 -*-
import time
import platform
if platform.system() == "Windows":
from wxUI import commonMessageDialogs, menus
from controller import user
elif platform.system() == "Linux":
from gi.repository import Gtk
from gtkUI import dialogs, commonMessageDialogs
from controller import messages
import widgetUtils
import webbrowser
import output
@@ -16,7 +8,9 @@ import logging
from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException
from pubsub import pub
from controller.twitter import user, messages
from sessions.twitter import compose, templates
from wxUI import commonMessageDialogs, menus
from . import base
log = logging.getLogger("controller.buffers.twitter.peopleBuffer")
@@ -252,4 +246,9 @@ class PeopleBuffer(base.BaseBuffer):
def get_item_url(self, *args, **kwargs):
tweet = self.get_tweet()
url = "https://twitter.com/{screen_name}".format(screen_name=tweet.screen_name)
return url
return url
def view_item(self):
item_url = self.get_item_url()
non_tweet = self.get_formatted_message()
msg = messages.viewTweet(non_tweet, [], False, item_url=item_url)

View File

@@ -1,15 +1,10 @@
# -*- coding: utf-8 -*-
import time
import platform
import locale
if platform.system() == "Windows":
from wxUI import commonMessageDialogs
elif platform.system() == "Linux":
from gi.repository import Gtk
from gtkUI import commonMessageDialogs
import widgetUtils
import logging
from tweepy.errors import TweepyException
from wxUI import commonMessageDialogs
from . import base, people
log = logging.getLogger("controller.buffers.twitter.searchBuffer")
@@ -67,6 +62,10 @@ class ConversationBuffer(SearchBuffer):
last_thread_id = None
last_reply_id = None
def __init__(self, tweet, *args, **kwargs):
self.tweet = tweet
super(ConversationBuffer, self).__init__(*args, **kwargs)
def start_stream(self, start=False, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory == True:

View File

@@ -1,20 +1,14 @@
# -*- coding: utf-8 -*-
import time
import platform
if platform.system() == "Windows":
import wx
from wxUI import buffers, commonMessageDialogs, menus
from controller import user, messages
elif platform.system() == "Linux":
from gi.repository import Gtk
from gtkUI import buffers, commonMessageDialogs
from controller import messages
import wx
import widgetUtils
import output
import logging
from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException
from pubsub import pub
from wxUI import buffers, commonMessageDialogs, menus
from controller.twitter import user, messages
from controller.buffers import base
log = logging.getLogger("controller.buffers.twitter.trends")
@@ -26,7 +20,7 @@ class TrendsBuffer(base.Buffer):
self.session = sessionObject
self.account = account
self.invisible = True
self.buffer = buffers.trendsPanel(parent, name)
self.buffer = buffers.twitter.trendsPanel(parent, name)
self.buffer.account = account
self.type = self.buffer.type
self.bind_events()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
import wx
import logging
from pubsub import pub
from wxUI.dialogs.mastodon import dialogs
from wxUI.dialogs.mastodon import search as search_dialogs
from wxUI.dialogs.mastodon import dialogs
from wxUI import commonMessageDialogs
from sessions.twitter import utils
from . import userActions
log = logging.getLogger("controller.mastodon.handler")
class Handler(object):
def __init__(self):
super(Handler, self).__init__()
def create_buffers(self, session, createAccounts=True, controller=None):
session.get_user_info()
name = session.get_name()
if createAccounts == True:
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
root_position =controller.view.search(name, name)
for i in session.settings['general']['buffer_order']:
if i == 'home':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Home"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="timeline_home", name="home_timeline", sessionObject=session, account=name, sound="tweet_received.ogg"))
elif i == 'local':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Local"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="timeline_local", name="local_timeline", sessionObject=session, account=name, sound="tweet_received.ogg"))
elif i == 'federated':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Federated"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="timeline_public", name="federated_timeline", sessionObject=session, account=name, sound="tweet_received.ogg"))
elif i == 'mentions':
pub.sendMessage("createBuffer", buffer_type="MentionsBuffer", session_type=session.type, buffer_title=_("Mentions"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="notifications", name="mentions", sessionObject=session, account=name, sound="mention_received.ogg"))
elif i == 'direct_messages':
pub.sendMessage("createBuffer", buffer_type="ConversationListBuffer", session_type=session.type, buffer_title=_("Direct messages"), parent_tab=root_position, start=False, kwargs=dict(compose_func="compose_conversation", parent=controller.view.nb, function="conversations", name="direct_messages", sessionObject=session, account=name, sound="dm_received.ogg"))
elif i == 'sent':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Sent"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="sent", sessionObject=session, account=name, sound="tweet_received.ogg", id=session.db["user_id"]))
elif i == 'favorites':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Favorites"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="favourites", name="favorites", sessionObject=session, account=name, sound="favourite.ogg"))
elif i == 'bookmarks':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Bookmarks"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="bookmarks", name="bookmarks", sessionObject=session, account=name, sound="favourite.ogg"))
elif i == 'followers':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Followers"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="followers", sessionObject=session, account=name, sound="update_followers.ogg", id=session.db["user_id"]))
elif i == 'following':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Following"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="following", sessionObject=session, account=name, sound="update_followers.ogg", id=session.db["user_id"]))
elif i == 'muted':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Muted users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="mutes", name="muted", sessionObject=session, account=name))
elif i == 'blocked':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="blocks", name="blocked", sessionObject=session, account=name))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="timelines", account=name))
timelines_position =controller.view.search("timelines", name)
for i in session.settings["other_buffers"]["timelines"]:
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=i, parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="%s-timeline".format(i), sessionObject=session, account=name, sound="tweet_timeline.ogg", id=i))
for i in session.settings["other_buffers"]["followers_timelines"]:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Followers for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="%s-followers" % (i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
for i in session.settings["other_buffers"]["following_timelines"]:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Following for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="%s-following" % (i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
# pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Lists"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="lists", name))
# lists_position =controller.view.search("lists", session.db["user_name"])
# for i in session.settings["other_buffers"]["lists"]:
# pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=session.type, buffer_title=_(u"List for {}").format(i), parent_tab=lists_position, start=False, kwargs=dict(parent=controller.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, name, bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Searches"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="searches", account=name))
# searches_position =controller.view.search("searches", session.db["user_name"])
# for i in session.settings["other_buffers"]["tweet_searches"]:
# pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_(u"Search for {}").format(i), parent_tab=searches_position, start=False, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (i,), sessionObject=session, name, bufferType="searchPanel", sound="search_updated.ogg", q=i, include_ext_alt_text=True, tweet_mode="extended"))
# for i in session.settings["other_buffers"]["trending_topic_buffers"]:
# pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (i), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (i,), sessionObject=session, name, trendsFor=i, sound="trends_updated.ogg"))
def start_buffer(self, controller, buffer):
if hasattr(buffer, "finished_timeline") and buffer.finished_timeline == False:
change_title = True
else:
change_title = False
try:
buffer.start_stream(play_sound=False)
except Exception as err:
log.exception("Error %s starting buffer %s on account %s, with args %r and kwargs %r." % (str(err), buffer.name, buffer.account, buffer.args, buffer.kwargs))
if change_title:
pub.sendMessage("buffer-title-changed", buffer=buffer)
def open_conversation(self, controller, buffer):
post = buffer.get_item()
if post.reblog != None:
post = post.reblog
conversations_position =controller.view.search("direct_messages", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="ConversationBuffer", session_type=buffer.session.type, buffer_title=_("Conversation with {0}").format(post.account.acct), parent_tab=conversations_position, start=True, kwargs=dict(parent=controller.view.nb, function="status_context", name="%s-conversation" % (post.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="search_updated.ogg", post=post, id=post.id))
def follow(self, buffer):
if not hasattr(buffer, "get_item"):
return
item = buffer.get_item()
if buffer.type == "user":
users = [item.acct]
elif buffer.type == "baseBuffer":
if item.reblog != None:
users = [user.acct for user in item.reblog.mentions if user.id != buffer.session.db["user_id"]]
if item.reblog.account.acct not in users and item.account.id != buffer.session.db["user_id"]:
users.insert(0, item.reblog.account.acct)
else:
users = [user.acct for user in item.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users:
users.insert(0, item.account.acct)
u = userActions.userActions(buffer.session, users)
def search(self, controller, session, value):
log.debug("Creating a new search...")
dlg = search_dialogs.searchDialog(value)
if dlg.ShowModal() == wx.ID_OK and dlg.term.GetValue() != "":
term = dlg.term.GetValue()
searches_position =controller.view.search("searches", session.get_name())
if dlg.posts.GetValue() == True:
if term not in session.settings["other_buffers"]["post_searches"]:
session.settings["other_buffers"]["post_searches"].append(term)
session.settings.write()
# pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", q=term, include_ext_alt_text=True, tweet_mode="extended"))
else:
log.error("A buffer for the %s search term is already created. You can't create a duplicate buffer." % (term,))
return
elif dlg.users.GetValue() == True:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_search", name="%s-searchUser" % (term,), sessionObject=session, account=session.get_name(), sound="search_updated.ogg", q=term))
dlg.Destroy()
# ToDo: explore how to play sound & save config differently.
# currently, TWBlue will play the sound and save the config for the timeline even if the buffer did not load or something else.
def open_timeline(self, controller, buffer):
if not hasattr(buffer, "get_item"):
return
item = buffer.get_item()
if buffer.type == "user":
users = [item.acct]
elif buffer.type == "baseBuffer":
if item.reblog != None:
users = [user.acct for user in item.reblog.mentions if user.id != buffer.session.db["user_id"]]
if item.reblog.account.acct not in users and item.account.id != buffer.session.db["user_id"]:
users.insert(0, item.reblog.account.acct)
else:
users = [user.acct for user in item.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users and item.account.id != buffer.session.db["user_id"]:
users.insert(0, item.account.acct)
u = userActions.UserTimeline(buffer.session, users)
if u.dialog.ShowModal() == wx.ID_OK:
action = u.process_action()
if action == None:
return
user = u.user
if action == "posts":
if user.statuses_count == 0:
dialogs.no_posts()
return
if user.id in buffer.session.settings["other_buffers"]["timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Timeline for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="%s-timeline" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", id=user.id))
buffer.session.settings["other_buffers"]["timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
elif action == "followers":
if user.followers_count == 0:
dialogs.no_followers()
return
if user.id in buffer.session.settings["other_buffers"]["followers_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Followers for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
buffer.session.settings["other_buffers"]["followers_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
elif action == "following":
if user.following_count == 0:
dialogs.no_following()
return
if user.id in buffer.session.settings["other_buffers"]["following_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Following for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
buffer.session.settings["other_buffers"]["following_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
buffer.session.settings.write()

View File

@@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
import os
import wx
import widgetUtils
import config
import output
from controller.twitter import messages
from sessions.mastodon import templates
from wxUI.dialogs.mastodon import postDialogs
class post(messages.basicTweet):
def __init__(self, session, title, caption, text="", *args, **kwargs):
# take max character limit from session as this might be different for some instances.
self.max = session.char_limit
self.title = title
self.session = session
self.message = postDialogs.Post(caption=caption, text=text, *args, **kwargs)
self.message.SetTitle(title)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
widgetUtils.connect_event(self.message.text, widgetUtils.ENTERED_TEXT, self.text_processor)
widgetUtils.connect_event(self.message.translate, widgetUtils.BUTTON_PRESSED, self.translate)
widgetUtils.connect_event(self.message.add, widgetUtils.BUTTON_PRESSED, self.on_attach)
widgetUtils.connect_event(self.message.remove_attachment, widgetUtils.BUTTON_PRESSED, self.remove_attachment)
# ToDo: Add autocomplete feature to mastodon and uncomment this.
# widgetUtils.connect_event(self.message.autocomplete_users, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
widgetUtils.connect_event(self.message.add_post, widgetUtils.BUTTON_PRESSED, self.add_post)
widgetUtils.connect_event(self.message.remove_post, widgetUtils.BUTTON_PRESSED, self.remove_post)
self.attachments = []
self.thread = []
self.text_processor()
def add_post(self, event, update_gui=True, *args, **kwargs):
text = self.message.text.GetValue()
attachments = self.attachments[::]
postdata = dict(text=text, attachments=attachments, sensitive=self.message.sensitive.GetValue(), spoiler_text=None)
if postdata.get("sensitive") == True:
postdata.update(spoiler_text=self.message.spoiler.GetValue())
self.thread.append(postdata)
self.attachments = []
if update_gui:
self.message.reset_controls()
self.message.add_item(item=[text, len(attachments)], list_type="post")
self.message.text.SetFocus()
self.text_processor()
def get_post_data(self):
self.add_post(event=None, update_gui=False)
return self.thread
def text_processor(self, *args, **kwargs):
super(post, self).text_processor(*args, **kwargs)
if len(self.thread) > 0:
if hasattr(self.message, "posts"):
self.message.posts.Enable(True)
self.message.remove_post.Enable(True)
else:
self.message.posts.Enable(False)
self.message.remove_post.Enable(False)
if len(self.attachments) > 0:
self.message.attachments.Enable(True)
self.message.remove_attachment.Enable(True)
else:
self.message.attachments.Enable(False)
self.message.remove_attachment.Enable(False)
if len(self.message.text.GetValue()) > 0 or len(self.attachments) > 0:
self.message.add_post.Enable(True)
else:
self.message.add_post.Enable(False)
def remove_post(self, *args, **kwargs):
post = self.message.posts.GetFocusedItem()
if post > -1 and len(self.thread) > post:
self.thread.pop(post)
self.message.remove_item(list_type="post")
self.text_processor()
self.message.text.SetFocus()
def can_attach(self):
if len(self.attachments) == 0:
return True
elif len(self.attachments) == 1 and (self.attachments[0]["type"] == "poll" or self.attachments[0]["type"] == "video" or self.attachments[0]["type"] == "audio"):
return False
elif len(self.attachments) < 4:
return True
return False
def on_attach(self, *args, **kwargs):
can_attach = self.can_attach()
menu = self.message.attach_menu(can_attach)
self.message.Bind(wx.EVT_MENU, self.on_attach_image, self.message.add_image)
self.message.Bind(wx.EVT_MENU, self.on_attach_video, self.message.add_video)
self.message.Bind(wx.EVT_MENU, self.on_attach_audio, self.message.add_audio)
self.message.Bind(wx.EVT_MENU, self.on_attach_poll, self.message.add_poll)
self.message.PopupMenu(menu, self.message.add.GetPosition())
def on_attach_image(self, *args, **kwargs):
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
image, description = self.message.get_image()
if image != None:
if image.endswith("gif"):
image_type = "gif"
else:
image_type = "photo"
imageInfo = {"type": image_type, "file": image, "description": description}
if len(self.attachments) > 0 and image_type == "gif":
return self.message.unable_to_attach_file()
self.attachments.append(imageInfo)
self.message.add_item(item=[os.path.basename(imageInfo["file"]), imageInfo["type"], imageInfo["description"]])
self.text_processor()
def on_attach_video(self, *args, **kwargs):
if len(self.attachments) >= 4:
return self.message.unable_to_attach_file()
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
video = self.message.get_video()
if video != None:
videoInfo = {"type": "video", "file": video, "description": ""}
self.attachments.append(videoInfo)
self.message.add_item(item=[os.path.basename(videoInfo["file"]), videoInfo["type"], videoInfo["description"]])
self.text_processor()
def on_attach_audio(self, *args, **kwargs):
if len(self.attachments) >= 4:
return self.message.unable_to_attach_file()
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
audio = self.message.get_audio()
if audio != None:
audioInfo = {"type": "audio", "file": audio, "description": ""}
self.attachments.append(audioInfo)
self.message.add_item(item=[os.path.basename(audioInfo["file"]), audioInfo["type"], audioInfo["description"]])
self.text_processor()
def on_attach_poll(self, *args, **kwargs):
if len(self.attachments) > 0:
return self.message.unable_to_attach_poll()
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
dlg = postDialogs.poll()
if dlg.ShowModal() == wx.ID_OK:
day = 86400
periods = [300, 1800, 3600, 21600, day, day*2, day*3, day*4, day*5, day*6, day*7]
period = periods[dlg.period.GetSelection()]
poll_options = dlg.get_options()
multiple = dlg.multiple.GetValue()
hide_totals = dlg.hide_votes.GetValue()
data = dict(type="poll", file="", description=_("Poll with {} options").format(len(poll_options)), options=poll_options, expires_in=period, multiple=multiple, hide_totals=hide_totals)
self.attachments.append(data)
self.message.add_item(item=[data["file"], data["type"], data["description"]])
self.text_processor()
dlg.Destroy()
def get_data(self):
self.add_post(event=None, update_gui=False)
return self.thread
def get_visibility(self):
visibility_settings = ["public", "unlisted", "private", "direct"]
return visibility_settings[self.message.visibility.GetSelection()]
class viewPost(post):
def __init__(self, post, offset_hours=0, date="", item_url=""):
if post.reblog != None:
post = post.reblog
author = post.account.display_name if post.account.display_name != "" else post.account.username
title = _(u"Post from {}").format(author)
image_description = templates.process_image_descriptions(post.media_attachments)
text = templates.process_text(post, safe=False)
date = templates.process_date(post.created_at, relative_times=False, offset_hours=offset_hours)
privacy_settings = dict(public=_("Public"), unlisted=_("Not listed"), private=_("followers only"), direct=_("Direct"))
privacy = privacy_settings.get(post.visibility)
boost_count = str(post.reblogs_count)
favs_count = str(post.favourites_count)
# Gets the client from where this post was made.
source_obj = post.get("application")
if source_obj == None:
source = _("Remote instance")
else:
source = source_obj.get("name")
self.message = postDialogs.viewPost(text=text, boosts_count=boost_count, favs_count=favs_count, source=source, date=date, privacy=privacy)
self.message.SetTitle(title)
if image_description != "":
self.message.image_description.Enable(True)
self.message.image_description.ChangeValue(image_description)
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
if item_url != "":
self.message.enable_button("share")
widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share)
self.item_url = item_url
widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate)
self.message.ShowModal()
# We won't need text_processor in this dialog, so let's avoid it.
def text_processor(self):
pass
def share(self, *args, **kwargs):
if hasattr(self, "item_url"):
output.copy(self.item_url)
output.speak(_("Link copied to clipboard."))

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
import logging
import widgetUtils
import output
from wxUI.dialogs.mastodon import userActions as userActionsDialog
from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog
from pubsub import pub
from mastodon import MastodonError, MastodonNotFoundError
from extra.autocompletionUsers import completion
log = logging.getLogger("controller.mastodon.userActions")
class BasicUserSelector(object):
def __init__(self, session, users=[]):
super(BasicUserSelector, self).__init__()
self.session = session
self.create_dialog(users=users)
def create_dialog(self, users):
pass
def autocomplete_users(self, *args, **kwargs):
c = completion.autocompletionUsers(self.dialog, self.session.session_id)
c.show_menu("dm")
def search_user(self, user):
try:
user = self.session.api.account_lookup(user)
return user
except MastodonError:
log.exception("Error searching for user %s.".format(user))
class userActions(BasicUserSelector):
def __init__(self, *args, **kwargs):
super(userActions, self).__init__(*args, **kwargs)
if self.dialog.get_response() == widgetUtils.OK:
self.process_action()
def create_dialog(self, users):
self.dialog = userActionsDialog.UserActionsDialog(users)
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
def process_action(self):
action = self.dialog.get_action()
user = self.dialog.get_user()
user = self.search_user(user)
if user == None:
return
getattr(self, action)(user)
def follow(self, user):
try:
self.session.api.account_follow(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def unfollow(self, user):
try:
result = self.session.api.account_unfollow(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def mute(self, user):
try:
id = self.session.api.account_mute(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def unmute(self, user):
try:
id = self.session.api.account_unmute(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def block(self, user):
try:
id = self.session.api.account_block(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def unblock(self, user):
try:
id = self.session.api.account_unblock(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
class UserTimeline(BasicUserSelector):
def create_dialog(self, users):
self.dialog = userTimelineDialog.UserTimeline(users)
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
def process_action(self):
action = self.dialog.get_action()
user = self.dialog.get_user()
user = self.search_user(user)
if user == None:
return
self.user = user
return action

View File

@@ -1,25 +1,14 @@
# -*- coding: utf-8 -*-
import os
import webbrowser
import threading
import logging
import sound_lib
import paths
import widgetUtils
import config
import languageHandler
import output
import application
import config_utils
import keys
from collections import OrderedDict
from pubsub import pub
from mysc import autostart as autostart_windows
from wxUI.dialogs import configuration
from wxUI import commonMessageDialogs
from extra.autocompletionUsers import scan, manage
from extra.ocr import OCRSpace
from .editTemplateController import EditTemplate
log = logging.getLogger("Settings")
@@ -132,234 +121,3 @@ class globalSettingsController(object):
config.app["proxy"]["user"] = self.dialog.get_value("proxy", "user")
config.app["proxy"]["password"] = self.dialog.get_value("proxy", "password")
config.app.write()
class accountSettingsController(globalSettingsController):
def __init__(self, buffer, window):
self.user = buffer.session.db["user_name"]
self.buffer = buffer
self.window = window
self.config = buffer.session.settings
super(accountSettingsController, self).__init__()
def create_config(self):
self.dialog.create_general_account()
widgetUtils.connect_event(self.dialog.general.userAutocompletionScan, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_scan)
widgetUtils.connect_event(self.dialog.general.userAutocompletionManage, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_manage)
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "show_screen_names", self.config["general"]["show_screen_names"])
self.dialog.set_value("general", "hide_emojis", self.config["general"]["hide_emojis"])
self.dialog.set_value("general", "itemsPerApiCall", self.config["general"]["max_tweets_per_call"])
self.dialog.set_value("general", "reverse_timelines", self.config["general"]["reverse_timelines"])
rt = self.config["general"]["retweet_mode"]
if rt == "ask":
self.dialog.set_value("general", "retweet_mode", _(u"Ask"))
elif rt == "direct":
self.dialog.set_value("general", "retweet_mode", _(u"Retweet without comments"))
else:
self.dialog.set_value("general", "retweet_mode", _(u"Retweet with comments"))
self.dialog.set_value("general", "persist_size", str(self.config["general"]["persist_size"]))
self.dialog.set_value("general", "load_cache_in_memory", self.config["general"]["load_cache_in_memory"])
self.dialog.create_reporting()
self.dialog.set_value("reporting", "speech_reporting", self.config["reporting"]["speech_reporting"])
self.dialog.set_value("reporting", "braille_reporting", self.config["reporting"]["braille_reporting"])
tweet_template = self.config["templates"]["tweet"]
dm_template = self.config["templates"]["dm"]
sent_dm_template = self.config["templates"]["dm_sent"]
person_template = self.config["templates"]["person"]
self.dialog.create_templates(tweet_template=tweet_template, dm_template=dm_template, sent_dm_template=sent_dm_template, person_template=person_template)
widgetUtils.connect_event(self.dialog.templates.tweet, widgetUtils.BUTTON_PRESSED, self.edit_tweet_template)
widgetUtils.connect_event(self.dialog.templates.dm, widgetUtils.BUTTON_PRESSED, self.edit_dm_template)
widgetUtils.connect_event(self.dialog.templates.sent_dm, widgetUtils.BUTTON_PRESSED, self.edit_sent_dm_template)
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
self.dialog.create_other_buffers()
buffer_values = self.get_buffers_list()
self.dialog.buffers.insert_buffers(buffer_values)
self.dialog.buffers.connect_hook_func(self.toggle_buffer_active)
widgetUtils.connect_event(self.dialog.buffers.toggle_state, widgetUtils.BUTTON_PRESSED, self.toggle_state)
widgetUtils.connect_event(self.dialog.buffers.up, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_up)
widgetUtils.connect_event(self.dialog.buffers.down, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_down)
self.dialog.create_ignored_clients(self.config["twitter"]["ignored_clients"])
widgetUtils.connect_event(self.dialog.ignored_clients.add, widgetUtils.BUTTON_PRESSED, self.add_ignored_client)
widgetUtils.connect_event(self.dialog.ignored_clients.remove, widgetUtils.BUTTON_PRESSED, self.remove_ignored_client)
self.input_devices = sound_lib.input.Input.get_device_names()
self.output_devices = sound_lib.output.Output.get_device_names()
self.soundpacks = []
[self.soundpacks.append(i) for i in os.listdir(paths.sound_path()) if os.path.isdir(os.path.join(paths.sound_path(), i)) == True ]
self.dialog.create_sound(self.input_devices, self.output_devices, self.soundpacks)
self.dialog.set_value("sound", "volumeCtrl", self.config["sound"]["volume"]*100)
self.dialog.set_value("sound", "input", self.config["sound"]["input_device"])
self.dialog.set_value("sound", "output", self.config["sound"]["output_device"])
self.dialog.set_value("sound", "session_mute", self.config["sound"]["session_mute"])
self.dialog.set_value("sound", "soundpack", self.config["sound"]["current_soundpack"])
self.dialog.set_value("sound", "indicate_audio", self.config["sound"]["indicate_audio"])
self.dialog.set_value("sound", "indicate_geo", self.config["sound"]["indicate_geo"])
self.dialog.set_value("sound", "indicate_img", self.config["sound"]["indicate_img"])
self.dialog.create_extras(OCRSpace.translatable_langs)
self.dialog.set_value("extras", "sndup_apiKey", self.config["sound"]["sndup_api_key"])
language_index = OCRSpace.OcrLangs.index(self.config["mysc"]["ocr_language"])
self.dialog.extras.ocr_lang.SetSelection(language_index)
self.dialog.realize()
self.dialog.set_title(_(u"Account settings for %s") % (self.user,))
self.response = self.dialog.get_response()
def edit_tweet_template(self, *args, **kwargs):
template = self.config["templates"]["tweet"]
control = EditTemplate(template=template, type="tweet")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["tweet"] = result
self.config.write()
self.dialog.templates.tweet.SetLabel(_("Edit template for tweets. Current template: {}").format(result))
def edit_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm"] = result
self.config.write()
self.dialog.templates.dm.SetLabel(_("Edit template for direct messages. Current template: {}").format(result))
def edit_sent_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm_sent"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm_sent"] = result
self.config.write()
self.dialog.templates.sent_dm.SetLabel(_("Edit template for sent direct messages. Current template: {}").format(result))
def edit_person_template(self, *args, **kwargs):
template = self.config["templates"]["person"]
control = EditTemplate(template=template, type="person")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["person"] = result
self.config.write()
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
def save_configuration(self):
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
self.needs_restart = True
log.debug("Triggered app restart due to change in relative times.")
self.config["general"]["relative_times"] = self.dialog.get_value("general", "relative_time")
self.config["general"]["show_screen_names"] = self.dialog.get_value("general", "show_screen_names")
self.config["general"]["hide_emojis"] = self.dialog.get_value("general", "hide_emojis")
self.config["general"]["max_tweets_per_call"] = self.dialog.get_value("general", "itemsPerApiCall")
if self.config["general"]["load_cache_in_memory"] != self.dialog.get_value("general", "load_cache_in_memory"):
self.config["general"]["load_cache_in_memory"] = self.dialog.get_value("general", "load_cache_in_memory")
self.needs_restart = True
log.debug("Triggered app restart due to change in database strategy management.")
if self.config["general"]["persist_size"] != self.dialog.get_value("general", "persist_size"):
if self.dialog.get_value("general", "persist_size") == '':
self.config["general"]["persist_size"] =-1
else:
try:
self.config["general"]["persist_size"] = int(self.dialog.get_value("general", "persist_size"))
except ValueError:
output.speak("Invalid cache size, setting to default.",True)
self.config["general"]["persist_size"] =1764
if self.config["general"]["reverse_timelines"] != self.dialog.get_value("general", "reverse_timelines"):
self.needs_restart = True
log.debug("Triggered app restart due to change in timeline order.")
self.config["general"]["reverse_timelines"] = self.dialog.get_value("general", "reverse_timelines")
rt = self.dialog.get_value("general", "retweet_mode")
if rt == _(u"Ask"):
self.config["general"]["retweet_mode"] = "ask"
elif rt == _(u"Retweet without comments"):
self.config["general"]["retweet_mode"] = "direct"
else:
self.config["general"]["retweet_mode"] = "comment"
buffers_list = self.dialog.buffers.get_list()
if buffers_list != self.config["general"]["buffer_order"]:
self.needs_restart = True
log.debug("Triggered app restart due to change in buffer ordering.")
self.config["general"]["buffer_order"] = buffers_list
self.config["reporting"]["speech_reporting"] = self.dialog.get_value("reporting", "speech_reporting")
self.config["reporting"]["braille_reporting"] = self.dialog.get_value("reporting", "braille_reporting")
self.config["mysc"]["ocr_language"] = OCRSpace.OcrLangs[self.dialog.extras.ocr_lang.GetSelection()]
if self.config["sound"]["input_device"] != self.dialog.sound.get("input"):
self.config["sound"]["input_device"] = self.dialog.sound.get("input")
try:
self.buffer.session.sound.input.set_device(self.buffer.session.sound.input.find_device_by_name(self.config["sound"]["input_device"]))
except:
self.config["sound"]["input_device"] = "default"
if self.config["sound"]["output_device"] != self.dialog.sound.get("output"):
self.config["sound"]["output_device"] = self.dialog.sound.get("output")
try:
self.buffer.session.sound.output.set_device(self.buffer.session.sound.output.find_device_by_name(self.config["sound"]["output_device"]))
except:
self.config["sound"]["output_device"] = "default"
self.config["sound"]["volume"] = self.dialog.get_value("sound", "volumeCtrl")/100.0
self.config["sound"]["session_mute"] = self.dialog.get_value("sound", "session_mute")
self.config["sound"]["current_soundpack"] = self.dialog.sound.get("soundpack")
self.config["sound"]["indicate_audio"] = self.dialog.get_value("sound", "indicate_audio")
self.config["sound"]["indicate_geo"] = self.dialog.get_value("sound", "indicate_geo")
self.config["sound"]["indicate_img"] = self.dialog.get_value("sound", "indicate_img")
self.config["sound"]["sndup_api_key"] = self.dialog.get_value("extras", "sndup_apiKey")
self.buffer.session.sound.config = self.config["sound"]
self.buffer.session.sound.check_soundpack()
self.config.write()
def toggle_state(self,*args,**kwargs):
return self.dialog.buffers.change_selected_item()
def on_autocompletion_scan(self, *args, **kwargs):
configuration = scan.autocompletionScan(self.buffer.session.settings, self.buffer, self.window)
to_scan = configuration.show_dialog()
if to_scan == True:
configuration.prepare_progress_dialog()
t = threading.Thread(target=configuration.scan)
t.start()
def on_autocompletion_manage(self, *args, **kwargs):
configuration = manage.autocompletionManage(self.buffer.session)
configuration.show_settings()
def add_ignored_client(self, *args, **kwargs):
client = commonMessageDialogs.get_ignored_client()
if client == None: return
if client not in self.config["twitter"]["ignored_clients"]:
self.config["twitter"]["ignored_clients"].append(client)
self.dialog.ignored_clients.append(client)
def remove_ignored_client(self, *args, **kwargs):
if self.dialog.ignored_clients.get_clients() == 0: return
id = self.dialog.ignored_clients.get_client_id()
self.config["twitter"]["ignored_clients"].pop(id)
self.dialog.ignored_clients.remove_(id)
def get_buffers_list(self):
all_buffers=OrderedDict()
all_buffers['home']=_(u"Home")
all_buffers['mentions']=_(u"Mentions")
all_buffers['dm']=_(u"Direct Messages")
all_buffers['sent_dm']=_(u"Sent direct messages")
all_buffers['sent_tweets']=_(u"Sent tweets")
all_buffers['favorites']=_(u"Likes")
all_buffers['followers']=_(u"Followers")
all_buffers['friends']=_(u"Friends")
all_buffers['blocks']=_(u"Blocked users")
all_buffers['muted']=_(u"Muted users")
list_buffers = []
hidden_buffers=[]
all_buffers_keys = list(all_buffers.keys())
# Check buffers shown first.
for i in self.config["general"]["buffer_order"]:
if i in all_buffers_keys:
list_buffers.append((i, all_buffers[i], True))
# This second pass will retrieve all hidden buffers.
for i in all_buffers_keys:
if i not in self.config["general"]["buffer_order"]:
hidden_buffers.append((i, all_buffers[i], False))
list_buffers.extend(hidden_buffers)
return list_buffers
def toggle_buffer_active(self, ev):
change = self.dialog.buffers.get_event(ev)
if change == True:
self.dialog.buffers.change_selected_item()

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,340 @@
# -*- coding: utf-8 -*-
import logging
import widgetUtils
import output
from pubsub import pub
from tweepy.errors import TweepyException, Forbidden
from mysc import restart
from sessions.twitter import utils, compose
from controller import userSelector
from wxUI import dialogs, commonMessageDialogs
from . import filters, lists, settings, userActions, trendingTopics, user
log = logging.getLogger("controller.twitter.handler")
class Handler(object):
def __init__(self):
super(Handler, self).__init__()
def create_buffers(self, session, createAccounts=True, controller=None):
session.get_user_info()
name = session.get_name()
if createAccounts == True:
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
root_position =controller.view.search(name, name)
for i in session.settings['general']['buffer_order']:
if i == 'home':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Home"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="home_timeline", name="home_timeline", sessionObject=session, account=session.get_name(), sound="tweet_received.ogg", include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'mentions':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Mentions"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="mentions_timeline", name="mentions", sessionObject=session, account=session.get_name(), sound="mention_received.ogg", include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'dm':
pub.sendMessage("createBuffer", buffer_type="DirectMessagesBuffer", session_type=session.type, buffer_title=_("Direct messages"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_direct_messages", name="direct_messages", sessionObject=session, account=session.get_name(), bufferType="dmPanel", compose_func="compose_direct_message", sound="dm_received.ogg"))
elif i == 'sent_dm':
pub.sendMessage("createBuffer", buffer_type="SentDirectMessagesBuffer", session_type=session.type, buffer_title=_("Sent direct messages"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function=None, name="sent_direct_messages", sessionObject=session, account=session.get_name(), bufferType="dmPanel", compose_func="compose_direct_message"))
elif i == 'sent_tweets':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Sent tweets"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="user_timeline", name="sent_tweets", sessionObject=session, account=session.get_name(), screen_name=session.db["user_name"], include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'favorites':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_favorites", name="favourites", sessionObject=session, account=session.get_name(), sound="favourite.ogg", include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'followers':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Followers"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_followers", name="followers", sessionObject=session, account=session.get_name(), sound="update_followers.ogg", screen_name=session.db["user_name"]))
elif i == 'friends':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Following"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_friends", name="friends", sessionObject=session, account=session.get_name(), screen_name=session.db["user_name"]))
elif i == 'blocks':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_blocks", name="blocked", sessionObject=session, account=session.get_name()))
elif i == 'muted':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Muted users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_mutes", name="muted", sessionObject=session, account=session.get_name()))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="timelines", account=name))
timelines_position =controller.view.search("timelines", name)
for i in session.settings["other_buffers"]["timelines"]:
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_(u"Timeline for {}").format(i,), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="user_timeline", name="%s-timeline" % (i,), sessionObject=session, account=session.get_name(), sound="tweet_timeline.ogg", bufferType=None, user_id=i, include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Likes timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="favs_timelines", account=name))
favs_timelines_position =controller.view.search("favs_timelines", name)
for i in session.settings["other_buffers"]["favourites_timelines"]:
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes for {}").format(i,), parent_tab=favs_timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_favorites", name="%s-favorite" % (i,), sessionObject=session, account=name, bufferType=None, sound="favourites_timeline_updated.ogg", user_id=i, include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Followers timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="followers_timelines", account=session.get_name()))
followers_timelines_position =controller.view.search("followers_timelines", name)
for i in session.settings["other_buffers"]["followers_timelines"]:
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Followers for {}").format(i,), parent_tab=followers_timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_followers", name="%s-followers" % (i,), sessionObject=session, account=session.get_name(), sound="new_event.ogg", user_id=i))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Following timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="friends_timelines", account=name))
friends_timelines_position =controller.view.search("friends_timelines", name)
for i in session.settings["other_buffers"]["friends_timelines"]:
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_(u"Friends for {}").format(i,), parent_tab=friends_timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_friends", name="%s-friends" % (i,), sessionObject=session, account=session.get_name(), sound="new_event.ogg", user_id=i))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Lists"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="lists", account=name))
lists_position =controller.view.search("lists", name)
for i in session.settings["other_buffers"]["lists"]:
pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=session.type, buffer_title=_(u"List for {}").format(i), parent_tab=lists_position, start=False, kwargs=dict(parent=controller.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, account=session.get_name(), bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Searches"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="searches", account=name))
searches_position =controller.view.search("searches", name)
for i in session.settings["other_buffers"]["tweet_searches"]:
pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_(u"Search for {}").format(i), parent_tab=searches_position, start=False, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (i,), sessionObject=session, account=session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", q=i, include_ext_alt_text=True, tweet_mode="extended"))
for i in session.settings["other_buffers"]["trending_topic_buffers"]:
pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (i), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (i,), sessionObject=session, account=session.get_name(), trendsFor=i, sound="trends_updated.ogg"))
def filter(self, buffer):
# Let's prevent filtering of some buffers (people buffers, direct messages, events and sent items).
if (buffer.name == "direct_messages" or buffer.name == "sent_tweets") or buffer.type == "people":
output.speak(_("Filters cannot be applied on this buffer"))
return
new_filter = filters.filter(buffer)
def manage_filters(self, session):
manage_filters = filters.filterManager(session)
def view_user_lists(self, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
selector = userSelector.userSelector(users=users, session_id=buffer.session.session_id)
user = selector.get_user()
if user == None:
return
l = lists.listsController(buffer.session, user=user)
def add_to_list(self, controller, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
selector = userSelector.userSelector(users=users, session_id=buffer.session.session_id)
user = selector.get_user()
if user == None:
return
dlg = dialogs.lists.addUserListDialog()
dlg.populate_list([compose.compose_list(item) for item in buffer.session.db["lists"]])
if dlg.get_response() == widgetUtils.OK:
try:
list = buffer.session.twitter.add_list_member(list_id=buffer.session.db["lists"][dlg.get_item()].id, screen_name=user)
older_list = utils.find_item(buffer.session.db["lists"][dlg.get_item()].id, buffer.session.db["lists"])
listBuffer = controller.search_buffer("%s-list" % (buffer.session.db["lists"][dlg.get_item()].name.lower()), buff.session.get_name())
if listBuffer != None:
listBuffer.get_user_ids()
buffer.session.db["lists"].pop(older_list)
buffer.session.db["lists"].append(list)
except TweepyException as e:
log.exception("error %s" % (str(e)))
output.speak("error %s" % (str(e)))
def remove_from_list(self, controller, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
selector = userSelector.userSelector(users=users, session_id=buffer.session.session_id)
user = selector.get_user()
if user == None:
return
dlg = dialogs.lists.removeUserListDialog()
dlg.populate_list([compose.compose_list(item) for item in buffer.session.db["lists"]])
if dlg.get_response() == widgetUtils.OK:
try:
list = buffer.session.twitter.remove_list_member(list_id=buffer.session.db["lists"][dlg.get_item()].id, screen_name=user)
older_list = utils.find_item(buffer.session.db["lists"][dlg.get_item()].id, buffer.session.db["lists"])
listBuffer = controller.search_buffer("%s-list" % (buffer.session.db["lists"][dlg.get_item()].name.lower()), buffer.session.get_name())
if listBuffer != None:
listBuffer.get_user_ids()
buffer.session.db["lists"].pop(older_list)
buffer.session.db["lists"].append(list)
except TweepyException as e:
output.speak("error %s" % (str(e)))
log.exception("error %s" % (str(e)))
def list_manager(self, session, lists_buffer_position):
return lists.listsController(session=session, lists_buffer_position=lists_buffer_position)
def account_settings(self, buffer, controller):
d = settings.accountSettingsController(buffer, controller)
if d.response == widgetUtils.OK:
d.save_configuration()
if d.needs_restart == True:
commonMessageDialogs.needs_restart()
buffer.session.settings.write()
buffer.session.save_persistent_data()
restart.restart_program()
def follow(self, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
u = userActions.userActionsController(buffer, users)
def add_alias(self, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
dlg = dialogs.userAliasDialogs.addAliasDialog(_("Add an user alias"), users)
if dlg.get_response() == widgetUtils.OK:
user, alias = dlg.get_user()
if user == "" or alias == "":
return
user_id = buffer.session.get_user_by_screen_name(user)
buffer.session.settings["user-aliases"][str(user_id)] = alias
buffer.session.settings.write()
output.speak(_("Alias has been set correctly for {}.").format(user))
pub.sendMessage("alias-added")
# ToDo: explore how to play sound & save config differently.
# currently, TWBlue will play the sound and save the config for the timeline even if the buffer did not load or something else.
def open_timeline(self, controller, buffer, default="tweets"):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
dlg = dialogs.userSelection.selectUserDialog(users=users, default=default)
if dlg.get_response() == widgetUtils.OK:
usr = utils.if_user_exists(buffer.session.twitter, dlg.get_user())
if usr != None:
if usr == dlg.get_user():
commonMessageDialogs.suspended_user()
return
if usr.protected == True:
if usr.following == False:
commonMessageDialogs.no_following()
return
tl_type = dlg.get_action()
if tl_type == "tweets":
if usr.statuses_count == 0:
commonMessageDialogs.no_tweets()
return
if usr.id_str in buffer.session.settings["other_buffers"]["timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Timeline for {}").format(usr.screen_name,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="user_timeline", name="%s-timeline" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", bufferType=None, user_id=usr.id_str, include_ext_alt_text=True, tweet_mode="extended"))
buffer.session.settings["other_buffers"]["timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
elif tl_type == "favourites":
if usr.favourites_count == 0:
commonMessageDialogs.no_favs()
return
if usr.id_str in buffer.session.settings["other_buffers"]["favourites_timelines"]:
commonMessageDialogs.timeline_exist()
return
favs_timelines_position =controller.view.search("favs_timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Likes for {}").format(usr.screen_name,), parent_tab=favs_timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="get_favorites", name="%s-favorite" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), bufferType=None, sound="favourites_timeline_updated.ogg", user_id=usr.id_str, include_ext_alt_text=True, tweet_mode="extended"))
buffer.session.settings["other_buffers"]["favourites_timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
elif tl_type == "followers":
if usr.followers_count == 0:
commonMessageDialogs.no_followers()
return
if usr.id_str in buffer.session.settings["other_buffers"]["followers_timelines"]:
commonMessageDialogs.timeline_exist()
return
followers_timelines_position =controller.view.search("followers_timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=buffer.session.type, buffer_title=_("Followers for {}").format(usr.screen_name,), parent_tab=followers_timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="get_followers", name="%s-followers" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", user_id=usr.id_str))
buffer.session.settings["other_buffers"]["followers_timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
elif tl_type == "friends":
if usr.friends_count == 0:
commonMessageDialogs.no_friends()
return
if usr.id_str in buffer.session.settings["other_buffers"]["friends_timelines"]:
commonMessageDialogs.timeline_exist()
return
friends_timelines_position =controller.view.search("friends_timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=buffer.session.type, buffer_title=_("Friends for {}").format(usr.screen_name,), parent_tab=friends_timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="get_friends", name="%s-friends" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", user_id=usr.id_str))
buffer.session.settings["other_buffers"]["friends_timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
else:
commonMessageDialogs.user_not_exist()
buffer.session.settings.write()
def open_conversation(self, controller, buffer):
tweet = buffer.get_right_tweet()
if hasattr(tweet, "retweeted_status") and tweet.retweeted_status != None:
tweet = tweet.retweeted_status
user = buffer.session.get_user(tweet.user).screen_name
searches_position =controller.view.search("searches", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="ConversationBuffer", session_type=buffer.session.type, buffer_title=_(u"Conversation with {0}").format(user), parent_tab=searches_position, start=True, kwargs=dict(tweet=tweet, parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (tweet.id,), sessionObject=buffer.session, account=buffer.session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", since_id=tweet.id, q="@{0}".format(user)))
def get_trending_topics(self, controller, session):
trends = trendingTopics.trendingTopicsController(session)
if trends.dialog.get_response() == widgetUtils.OK:
woeid = trends.get_woeid()
if woeid in session.settings["other_buffers"]["trending_topic_buffers"]:
return
root_position =controller.view.search(session.get_name(), session.get_name())
pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (trends.get_string()), parent_tab=root_position, start=True, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (woeid,), sessionObject=session, account=session.get_name(), trendsFor=woeid, sound="trends_updated.ogg"))
session.settings["other_buffers"]["trending_topic_buffers"].append(str(woeid))
session.settings.write()
def start_buffer(self, controller, buffer):
if hasattr(buffer, "finished_timeline") and buffer.finished_timeline == False:
change_title = True
else:
change_title = False
try:
if "mentions" in buffer.name or "direct_messages" in buffer.name:
buffer.start_stream()
else:
buffer.start_stream(play_sound=False)
except TweepyException as err:
log.exception("Error %s starting buffer %s on account %s, with args %r and kwargs %r." % (str(err), buffer.name, buffer.account, buffer.args, buffer.kwargs))
# Determine if this error was caused by a block applied to the current user (IE permission errors).
if type(err) == Forbidden:
buff = controller.view.search(buffer.name, buffer.account)
buffer.remove_buffer(force=True)
commonMessageDialogs.blocked_timeline()
if controller.get_current_buffer() == buffer:
controller.right()
controller.view.delete_buffer(buff)
controller.buffers.remove(buffer)
del buffer
if change_title:
pub.sendMessage("buffer-title-changed", buffer=buffer)
def update_profile(self, session):
r = user.profileController(session)
def search(self, controller, session, value):
log.debug("Creating a new search...")
dlg = dialogs.search.searchDialog(value)
if dlg.get_response() == widgetUtils.OK and dlg.get("term") != "":
term = dlg.get("term")
searches_position =controller.view.search("searches", session.get_name())
if dlg.get("tweets") == True:
if term not in session.settings["other_buffers"]["tweet_searches"]:
session.settings["other_buffers"]["tweet_searches"].append(term)
session.settings.write()
args = {"lang": dlg.get_language(), "result_type": dlg.get_result_type()}
pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", q=term, include_ext_alt_text=True, tweet_mode="extended"))
else:
log.error("A buffer for the %s search term is already created. You can't create a duplicate buffer." % (term,))
return
elif dlg.get("users") == True:
pub.sendMessage("createBuffer", buffer_type="SearchPeopleBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, function="search_users", name="%s-searchUser" % (term,), sessionObject=session, account=session.get_name(), bufferType=None, sound="search_updated.ogg", q=term))
dlg.Destroy()

View File

@@ -10,9 +10,10 @@ from pubsub import pub
log = logging.getLogger("controller.listsController")
class listsController(object):
def __init__(self, session, user=None):
def __init__(self, session, user=None, lists_buffer_position=0):
super(listsController, self).__init__()
self.session = session
self.lists_buffer_position = lists_buffer_position
if user == None:
self.dialog = lists.listViewer()
self.dialog.populate_list(self.get_all_lists())
@@ -90,7 +91,9 @@ class listsController(object):
def open_list_as_buffer(self, *args, **kwargs):
if self.dialog.lista.get_count() == 0: return
list = self.session.db["lists"][self.dialog.get_item()]
pub.sendMessage("create-new-buffer", buffer="list", account=self.session.db["user_name"], create=list.name)
pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=self.session.type, buffer_title=_("List for {}").format(list.name), parent_tab=self.lists_buffer_position, start=True, kwargs=dict(function="list_timeline", name="%s-list" % (list.name,), sessionObject=self.session, account=self.session.get_name(), bufferType=None, sound="list_tweet.ogg", list_id=list.id, include_ext_alt_text=True, tweet_mode="extended"))
self.session.settings["other_buffers"]["lists"].append(list.name)
self.session.settings.write()
def subscribe(self, *args, **kwargs):
if self.dialog.lista.get_count() == 0: return

View File

@@ -8,7 +8,7 @@ import output
import sound
import config
from pubsub import pub
from twitter_text import parse_tweet
from twitter_text.parse_tweet import parse_tweet
from wxUI.dialogs import twitterDialogs, urlList
from wxUI import commonMessageDialogs
from extra import translator, SpellChecker
@@ -23,7 +23,7 @@ class basicTweet(object):
self.max = max
self.title = title
self.session = session
self.message = getattr(twitterDialogs, messageType)(title=title, caption=caption, message=text, *args, **kwargs)
self.message = getattr(twitterDialogs, messageType)(title=title, caption=caption, message=text, max_length=max, *args, **kwargs)
self.message.text.SetValue(text)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
@@ -185,11 +185,12 @@ class tweet(basicTweet):
def text_processor(self, *args, **kwargs):
super(tweet, self).text_processor(*args, **kwargs)
if len(self.thread) > 0:
self.message.tweets.Enable(True)
self.message.remove_tweet.Enable(True)
else:
self.message.tweets.Enable(False)
self.message.remove_tweet.Enable(False)
if hasattr(self.message, "tweets"):
self.message.tweets.Enable(True)
self.message.remove_tweet.Enable(True)
else:
self.message.tweets.Enable(False)
self.message.remove_tweet.Enable(False)
if len(self.attachments) > 0:
self.message.attachments.Enable(True)
self.message.remove_attachment.Enable(True)

View File

@@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
import os
import threading
import logging
import sound_lib
import paths
import widgetUtils
import output
from collections import OrderedDict
from wxUI import commonMessageDialogs
from extra.autocompletionUsers import scan, manage
from extra.ocr import OCRSpace
from controller.settings import globalSettingsController
from . templateEditor import EditTemplate
log = logging.getLogger("Settings")
class accountSettingsController(globalSettingsController):
def __init__(self, buffer, window):
self.user = buffer.session.db["user_name"]
self.buffer = buffer
self.window = window
self.config = buffer.session.settings
super(accountSettingsController, self).__init__()
def create_config(self):
self.dialog.create_general_account()
widgetUtils.connect_event(self.dialog.general.userAutocompletionScan, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_scan)
widgetUtils.connect_event(self.dialog.general.userAutocompletionManage, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_manage)
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "show_screen_names", self.config["general"]["show_screen_names"])
self.dialog.set_value("general", "hide_emojis", self.config["general"]["hide_emojis"])
self.dialog.set_value("general", "itemsPerApiCall", self.config["general"]["max_tweets_per_call"])
self.dialog.set_value("general", "reverse_timelines", self.config["general"]["reverse_timelines"])
rt = self.config["general"]["retweet_mode"]
if rt == "ask":
self.dialog.set_value("general", "retweet_mode", _(u"Ask"))
elif rt == "direct":
self.dialog.set_value("general", "retweet_mode", _(u"Retweet without comments"))
else:
self.dialog.set_value("general", "retweet_mode", _(u"Retweet with comments"))
self.dialog.set_value("general", "persist_size", str(self.config["general"]["persist_size"]))
self.dialog.set_value("general", "load_cache_in_memory", self.config["general"]["load_cache_in_memory"])
self.dialog.create_reporting()
self.dialog.set_value("reporting", "speech_reporting", self.config["reporting"]["speech_reporting"])
self.dialog.set_value("reporting", "braille_reporting", self.config["reporting"]["braille_reporting"])
tweet_template = self.config["templates"]["tweet"]
dm_template = self.config["templates"]["dm"]
sent_dm_template = self.config["templates"]["dm_sent"]
person_template = self.config["templates"]["person"]
self.dialog.create_templates(tweet_template=tweet_template, dm_template=dm_template, sent_dm_template=sent_dm_template, person_template=person_template)
widgetUtils.connect_event(self.dialog.templates.tweet, widgetUtils.BUTTON_PRESSED, self.edit_tweet_template)
widgetUtils.connect_event(self.dialog.templates.dm, widgetUtils.BUTTON_PRESSED, self.edit_dm_template)
widgetUtils.connect_event(self.dialog.templates.sent_dm, widgetUtils.BUTTON_PRESSED, self.edit_sent_dm_template)
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
self.dialog.create_other_buffers()
buffer_values = self.get_buffers_list()
self.dialog.buffers.insert_buffers(buffer_values)
self.dialog.buffers.connect_hook_func(self.toggle_buffer_active)
widgetUtils.connect_event(self.dialog.buffers.toggle_state, widgetUtils.BUTTON_PRESSED, self.toggle_state)
widgetUtils.connect_event(self.dialog.buffers.up, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_up)
widgetUtils.connect_event(self.dialog.buffers.down, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_down)
self.dialog.create_ignored_clients(self.config["twitter"]["ignored_clients"])
widgetUtils.connect_event(self.dialog.ignored_clients.add, widgetUtils.BUTTON_PRESSED, self.add_ignored_client)
widgetUtils.connect_event(self.dialog.ignored_clients.remove, widgetUtils.BUTTON_PRESSED, self.remove_ignored_client)
self.input_devices = sound_lib.input.Input.get_device_names()
self.output_devices = sound_lib.output.Output.get_device_names()
self.soundpacks = []
[self.soundpacks.append(i) for i in os.listdir(paths.sound_path()) if os.path.isdir(os.path.join(paths.sound_path(), i)) == True ]
self.dialog.create_sound(self.input_devices, self.output_devices, self.soundpacks)
self.dialog.set_value("sound", "volumeCtrl", int(self.config["sound"]["volume"]*100))
self.dialog.set_value("sound", "input", self.config["sound"]["input_device"])
self.dialog.set_value("sound", "output", self.config["sound"]["output_device"])
self.dialog.set_value("sound", "session_mute", self.config["sound"]["session_mute"])
self.dialog.set_value("sound", "soundpack", self.config["sound"]["current_soundpack"])
self.dialog.set_value("sound", "indicate_audio", self.config["sound"]["indicate_audio"])
self.dialog.set_value("sound", "indicate_geo", self.config["sound"]["indicate_geo"])
self.dialog.set_value("sound", "indicate_img", self.config["sound"]["indicate_img"])
self.dialog.create_extras(OCRSpace.translatable_langs)
self.dialog.set_value("extras", "sndup_apiKey", self.config["sound"]["sndup_api_key"])
language_index = OCRSpace.OcrLangs.index(self.config["mysc"]["ocr_language"])
self.dialog.extras.ocr_lang.SetSelection(language_index)
self.dialog.realize()
self.dialog.set_title(_(u"Account settings for %s") % (self.user,))
self.response = self.dialog.get_response()
def edit_tweet_template(self, *args, **kwargs):
template = self.config["templates"]["tweet"]
control = EditTemplate(template=template, type="tweet")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["tweet"] = result
self.config.write()
self.dialog.templates.tweet.SetLabel(_("Edit template for tweets. Current template: {}").format(result))
def edit_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm"] = result
self.config.write()
self.dialog.templates.dm.SetLabel(_("Edit template for direct messages. Current template: {}").format(result))
def edit_sent_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm_sent"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm_sent"] = result
self.config.write()
self.dialog.templates.sent_dm.SetLabel(_("Edit template for sent direct messages. Current template: {}").format(result))
def edit_person_template(self, *args, **kwargs):
template = self.config["templates"]["person"]
control = EditTemplate(template=template, type="person")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["person"] = result
self.config.write()
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
def save_configuration(self):
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
self.needs_restart = True
log.debug("Triggered app restart due to change in relative times.")
self.config["general"]["relative_times"] = self.dialog.get_value("general", "relative_time")
self.config["general"]["show_screen_names"] = self.dialog.get_value("general", "show_screen_names")
self.config["general"]["hide_emojis"] = self.dialog.get_value("general", "hide_emojis")
self.config["general"]["max_tweets_per_call"] = self.dialog.get_value("general", "itemsPerApiCall")
if self.config["general"]["load_cache_in_memory"] != self.dialog.get_value("general", "load_cache_in_memory"):
self.config["general"]["load_cache_in_memory"] = self.dialog.get_value("general", "load_cache_in_memory")
self.needs_restart = True
log.debug("Triggered app restart due to change in database strategy management.")
if self.config["general"]["persist_size"] != self.dialog.get_value("general", "persist_size"):
if self.dialog.get_value("general", "persist_size") == '':
self.config["general"]["persist_size"] =-1
else:
try:
self.config["general"]["persist_size"] = int(self.dialog.get_value("general", "persist_size"))
except ValueError:
output.speak("Invalid cache size, setting to default.",True)
self.config["general"]["persist_size"] =1764
if self.config["general"]["reverse_timelines"] != self.dialog.get_value("general", "reverse_timelines"):
self.needs_restart = True
log.debug("Triggered app restart due to change in timeline order.")
self.config["general"]["reverse_timelines"] = self.dialog.get_value("general", "reverse_timelines")
rt = self.dialog.get_value("general", "retweet_mode")
if rt == _(u"Ask"):
self.config["general"]["retweet_mode"] = "ask"
elif rt == _(u"Retweet without comments"):
self.config["general"]["retweet_mode"] = "direct"
else:
self.config["general"]["retweet_mode"] = "comment"
buffers_list = self.dialog.buffers.get_list()
if buffers_list != self.config["general"]["buffer_order"]:
self.needs_restart = True
log.debug("Triggered app restart due to change in buffer ordering.")
self.config["general"]["buffer_order"] = buffers_list
self.config["reporting"]["speech_reporting"] = self.dialog.get_value("reporting", "speech_reporting")
self.config["reporting"]["braille_reporting"] = self.dialog.get_value("reporting", "braille_reporting")
self.config["mysc"]["ocr_language"] = OCRSpace.OcrLangs[self.dialog.extras.ocr_lang.GetSelection()]
if self.config["sound"]["input_device"] != self.dialog.sound.get("input"):
self.config["sound"]["input_device"] = self.dialog.sound.get("input")
try:
self.buffer.session.sound.input.set_device(self.buffer.session.sound.input.find_device_by_name(self.config["sound"]["input_device"]))
except:
self.config["sound"]["input_device"] = "default"
if self.config["sound"]["output_device"] != self.dialog.sound.get("output"):
self.config["sound"]["output_device"] = self.dialog.sound.get("output")
try:
self.buffer.session.sound.output.set_device(self.buffer.session.sound.output.find_device_by_name(self.config["sound"]["output_device"]))
except:
self.config["sound"]["output_device"] = "default"
self.config["sound"]["volume"] = self.dialog.get_value("sound", "volumeCtrl")/100.0
self.config["sound"]["session_mute"] = self.dialog.get_value("sound", "session_mute")
self.config["sound"]["current_soundpack"] = self.dialog.sound.get("soundpack")
self.config["sound"]["indicate_audio"] = self.dialog.get_value("sound", "indicate_audio")
self.config["sound"]["indicate_geo"] = self.dialog.get_value("sound", "indicate_geo")
self.config["sound"]["indicate_img"] = self.dialog.get_value("sound", "indicate_img")
self.config["sound"]["sndup_api_key"] = self.dialog.get_value("extras", "sndup_apiKey")
self.buffer.session.sound.config = self.config["sound"]
self.buffer.session.sound.check_soundpack()
self.config.write()
def toggle_state(self,*args,**kwargs):
return self.dialog.buffers.change_selected_item()
def on_autocompletion_scan(self, *args, **kwargs):
configuration = scan.autocompletionScan(self.buffer.session.settings, self.buffer, self.window)
to_scan = configuration.show_dialog()
if to_scan == True:
configuration.prepare_progress_dialog()
t = threading.Thread(target=configuration.scan)
t.start()
def on_autocompletion_manage(self, *args, **kwargs):
configuration = manage.autocompletionManage(self.buffer.session)
configuration.show_settings()
def add_ignored_client(self, *args, **kwargs):
client = commonMessageDialogs.get_ignored_client()
if client == None: return
if client not in self.config["twitter"]["ignored_clients"]:
self.config["twitter"]["ignored_clients"].append(client)
self.dialog.ignored_clients.append(client)
def remove_ignored_client(self, *args, **kwargs):
if self.dialog.ignored_clients.get_clients() == 0: return
id = self.dialog.ignored_clients.get_client_id()
self.config["twitter"]["ignored_clients"].pop(id)
self.dialog.ignored_clients.remove_(id)
def get_buffers_list(self):
all_buffers=OrderedDict()
all_buffers['home']=_(u"Home")
all_buffers['mentions']=_(u"Mentions")
all_buffers['dm']=_(u"Direct Messages")
all_buffers['sent_dm']=_(u"Sent direct messages")
all_buffers['sent_tweets']=_(u"Sent tweets")
all_buffers['favorites']=_(u"Likes")
all_buffers['followers']=_(u"Followers")
all_buffers['friends']=_(u"Friends")
all_buffers['blocks']=_(u"Blocked users")
all_buffers['muted']=_(u"Muted users")
list_buffers = []
hidden_buffers=[]
all_buffers_keys = list(all_buffers.keys())
# Check buffers shown first.
for i in self.config["general"]["buffer_order"]:
if i in all_buffers_keys:
list_buffers.append((i, all_buffers[i], True))
# This second pass will retrieve all hidden buffers.
for i in all_buffers_keys:
if i not in self.config["general"]["buffer_order"]:
hidden_buffers.append((i, all_buffers[i], False))
list_buffers.extend(hidden_buffers)
return list_buffers
def toggle_buffer_active(self, ev):
change = self.dialog.buffers.get_event(ev)
if change == True:
self.dialog.buffers.change_selected_item()

View File

@@ -29,42 +29,42 @@ class userActionsController(object):
def follow(self, user):
try:
self.session.twitter.create_friendship(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id)
pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err:
output.speak("Error %s" % (str(err)), True)
def unfollow(self, user):
try:
id = self.session.twitter.destroy_friendship(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id)
pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err:
output.speak("Error %s" % (str(err)), True)
def mute(self, user):
try:
id = self.session.twitter.create_mute(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id)
pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err:
output.speak("Error %s" % (str(err)), True)
def unmute(self, user):
try:
id = self.session.twitter.destroy_mute(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id)
pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err:
output.speak("Error %s" % (str(err)), True)
def report(self, user):
try:
id = self.session.twitter.report_spam(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id)
pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err:
output.speak("Error %s" % (str(err)), True)
def block(self, user):
try:
id = self.session.twitter.create_block(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id)
pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err:
output.speak("Error %s" % (str(err)), True)

View File

@@ -1,3 +1 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from .soundsTutorial import soundsTutorial

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from gi.repository import Gtk
import widgetUtils
class soundsTutorialDialog(Gtk.Dialog):
def __init__(self, actions):
super(soundsTutorialDialog, self).__init__("Sounds tutorial", None, 0, (Gtk.STOCK_CANCEL, widgetUtils.CANCEL))
box = self.get_content_area()
label = Gtk.Label("Press enter for listen the sound")
self.list = widgetUtils.list("Action")
self.populate_actions(actions)
lBox = Gtk.Box(spacing=6)
lBox.add(label)
lBox.add(self.list.list)
box.add(lBox)
self.play = Gtk.Button("Play")
box.add(self.play)
self.show_all()
def populate_actions(self, actions):
for i in actions:
self.list.insert_item(i)
def get_selected(self):
return self.list.get_selected()

View File

@@ -1,4 +1,3 @@
from __future__ import unicode_literals
#Reverse sort, by Bill Dengler <codeofdusk@gmail.com> for use in TWBlue http://twblue.es
def invert_tuples(t):
"Invert a list of tuples, so that the 0th element becomes the -1th, and the -1th becomes the 0th."

View File

@@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
from builtins import object
import platform
import widgetUtils
import os
@@ -9,10 +6,7 @@ import paths
import logging
log = logging.getLogger("extra.SoundsTutorial.soundsTutorial")
from . import soundsTutorial_constants
if platform.system() == "Windows":
from . import wx_ui as UI
elif platform.system() == "Linux":
from . import gtk_ui as UI
from . import wx_ui as UI
class soundsTutorial(object):
def __init__(self, sessionObject):

View File

@@ -1,7 +1,4 @@
#-*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
#-*- coding: utf-8 -*-
from . import reverse_sort
import application
actions = reverse_sort.reverse_sort([ ("audio", _(u"Audio tweet.")),

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import wx
import widgetUtils

View File

@@ -1,8 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
from builtins import next
from builtins import object
import os
import logging
from . import wx_ui

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
from enchant.tokenize import Filter

View File

@@ -16,7 +16,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
############################################################
from __future__ import unicode_literals
import wx
import application

View File

@@ -2,7 +2,4 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from . import translator
import platform
if platform.system() == "Windows":
from . import wx_ui as gui
from . import wx_ui as gui

View File

@@ -21,7 +21,7 @@ def parse(s):
lst.remove(item)
#end if
if len(lst) > 1: #more than one key, parse error
raise ValueError, 'unknown modifier %s' % lst[0]
raise ValueError('unknown modifier %s' % lst[0])
return (m, lst[0].lower())
class AtspiThread(threading.Thread):
def run(self):

View File

@@ -6,27 +6,26 @@ from wxUI.dialogs import baseDialog
class keystrokeEditorDialog(baseDialog.BaseWXDialog):
def __init__(self):
super(keystrokeEditorDialog, self).__init__(parent=None, id=-1, title=_(u"Keystroke editor"))
panel = wx.Panel(self)
self.actions = []
sizer = wx.BoxSizer(wx.VERTICAL)
keysText = wx.StaticText(panel, -1, _(u"Select a keystroke to edit"))
keysText = wx.StaticText(self, -1, _(u"Select a keystroke to edit"))
self.keys = widgets.list(self, _(u"Action"), _(u"Keystroke"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL, size=(400, 450))
self.keys.list.SetFocus()
firstSizer = wx.BoxSizer(wx.HORIZONTAL)
firstSizer.Add(keysText, 0, wx.ALL, 5)
firstSizer.Add(self.keys.list, 0, wx.ALL, 5)
self.edit = wx.Button(panel, -1, _(u"Edit"))
self.edit = wx.Button(self, -1, _(u"Edit"))
self.edit.SetDefault()
self.undefine = wx.Button(panel, -1, _("Undefine keystroke"))
self.execute = wx.Button(panel, -1, _(u"Execute action"))
close = wx.Button(panel, wx.ID_CANCEL, _(u"Close"))
self.undefine = wx.Button(self, -1, _("Undefine keystroke"))
self.execute = wx.Button(self, -1, _(u"Execute action"))
close = wx.Button(self, wx.ID_CANCEL, _(u"Close"))
secondSizer = wx.BoxSizer(wx.HORIZONTAL)
secondSizer.Add(self.edit, 0, wx.ALL, 5)
secondSizer.Add(self.execute, 0, wx.ALL, 5)
secondSizer.Add(close, 0, wx.ALL, 5)
sizer.Add(firstSizer, 0, wx.ALL, 5)
sizer.Add(secondSizer, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
def put_keystrokes(self, actions, keystrokes):

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,92 +1,82 @@
# Steffen Schultz <steffenschultz@mailbox.org>, 2022.
msgid ""
msgstr ""
"Project-Id-Version: TW Blue 0.80\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Report-Msgid-Bugs-To: manuel@manuelcortez.net\n"
"POT-Creation-Date: 2022-08-16 17:50-0500\n"
"PO-Revision-Date: 2019-03-17 15:50+0100\n"
"PO-Revision-Date: 2022-10-05 13:23+0000\n"
"Last-Translator: Steffen Schultz <steffenschultz@mailbox.org>\n"
"Language-Team: Steffen Schultz <schulle3o@yahoo.de>\n"
"Language-Team: German <https://weblate.mcvsoftware.com/projects/twblue/"
"twblue/de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.14\n"
"Generated-By: Babel 2.10.3\n"
#: languageHandler.py:61
#, fuzzy
msgctxt "languageName"
msgid "Amharic"
msgstr "Amharisch"
#: languageHandler.py:62
#, fuzzy
msgctxt "languageName"
msgid "Aragonese"
msgstr "Japanisch"
msgstr "Aragonesisch"
#: languageHandler.py:63
#, fuzzy
msgctxt "languageName"
msgid "Spanish"
msgstr "Spanisch"
#: languageHandler.py:64
#, fuzzy
msgctxt "languageName"
msgid "Portuguese"
msgstr "Portugiesisch"
#: languageHandler.py:65
#, fuzzy
msgctxt "languageName"
msgid "Russian"
msgstr "Russisch"
#: languageHandler.py:66
#, fuzzy
msgctxt "languageName"
msgid "italian"
msgstr "Italienisch"
#: languageHandler.py:67
#, fuzzy
msgctxt "languageName"
msgid "Turkey"
msgstr "Funktion"
msgstr "Türkisch"
#: languageHandler.py:68
#, fuzzy
msgctxt "languageName"
msgid "Galician"
msgstr "Galizisch"
#: languageHandler.py:69
#, fuzzy
msgctxt "languageName"
msgid "Catala"
msgstr "Katalanisch"
#: languageHandler.py:70
#, fuzzy
msgctxt "languageName"
msgid "Vasque"
msgstr "Baskisch"
#: languageHandler.py:71
#, fuzzy
msgctxt "languageName"
msgid "polish"
msgstr "Polnisch"
#: languageHandler.py:72
#, fuzzy
msgctxt "languageName"
msgid "Arabic"
msgstr "Arabisch"
#: languageHandler.py:73
#, fuzzy
msgctxt "languageName"
msgid "Nepali"
msgstr "Nepalesisch"
@@ -94,10 +84,9 @@ msgstr "Nepalesisch"
#: languageHandler.py:74
msgctxt "languageName"
msgid "Serbian (Latin)"
msgstr ""
msgstr "Serbisch (Latein)"
#: languageHandler.py:75
#, fuzzy
msgctxt "languageName"
msgid "Japanese"
msgstr "Japanisch"
@@ -168,9 +157,8 @@ msgid "Followers"
msgstr "Folger"
#: controller/mainController.py:351
#, fuzzy
msgid "Following"
msgstr "E&ntfolgen"
msgstr "Folge ich"
#: controller/buffers/twitter/base.py:70 controller/mainController.py:353
#: controller/mainController.py:1399 controller/settings.py:346
@@ -201,7 +189,6 @@ msgid "Likes for {}"
msgstr "Likes für {}"
#: controller/mainController.py:364
#, fuzzy
msgid "Followers timelines"
msgstr "Folger-Zeitleisten"
@@ -211,9 +198,8 @@ msgid "Followers for {}"
msgstr "Folger von {}"
#: controller/mainController.py:368
#, fuzzy
msgid "Following timelines"
msgstr "Folger-Zeitleisten"
msgstr "Freunde-Zeitleisten"
#: controller/mainController.py:371 controller/mainController.py:934
#: controller/mainController.py:1582
@@ -267,11 +253,11 @@ msgstr "Filter können für diese Ansicht nicht angewendet werden"
#: controller/mainController.py:747
msgid "Add an user alias"
msgstr ""
msgstr "Benutzer-Alias hinzufügen"
#: controller/mainController.py:755
msgid "Alias has been set correctly for {}."
msgstr ""
msgstr "Alias korrekt festgelegt für {}."
#: controller/mainController.py:823 controller/messages.py:328
msgid "MMM D, YYYY. H:m"
@@ -292,7 +278,7 @@ msgstr ""
#: controller/mainController.py:998
msgid "Unable to find address in OpenStreetMap."
msgstr ""
msgstr "Adresse kann nicht in OpenStreetMap gefunden werden."
#: controller/mainController.py:1011
msgid "There are no results for the coordinates in this tweet"
@@ -423,34 +409,32 @@ msgid "View item"
msgstr "Eintrag ansehen"
#: controller/messages.py:381
#, fuzzy
msgid "Link copied to clipboard."
msgstr "In Zwischenablage kopieren"
msgstr "Link in Zwischenablage kopiert."
#: controller/settings.py:77
#, fuzzy
msgid "System default"
msgstr "Benutzerstandard"
msgstr "Systemstandard"
#: controller/settings.py:77
msgid "HTTP"
msgstr ""
msgstr "HTTP"
#: controller/settings.py:77
msgid "SOCKS v4"
msgstr ""
msgstr "SOCKS v4"
#: controller/settings.py:77
msgid "SOCKS v4 with DNS support"
msgstr ""
msgstr "SOCKS v4 mit DNS-Unterstützung"
#: controller/settings.py:77
msgid "SOCKS v5"
msgstr ""
msgstr "SOCKS v5"
#: controller/settings.py:77
msgid "SOCKS v5 with DNS support"
msgstr ""
msgstr "SOCKS v5 mit DNS-Unterstützung"
#: controller/settings.py:155 controller/settings.py:269
#: wxUI/dialogs/configuration.py:121
@@ -473,19 +457,20 @@ msgstr "Account-Einstellungen für %s"
#: controller/settings.py:213 wxUI/dialogs/configuration.py:247
msgid "Edit template for tweets. Current template: {}"
msgstr ""
msgstr "Tweet-Vorlage bearbeiten. Aktuelle Vorlage: {}"
#: controller/settings.py:222 wxUI/dialogs/configuration.py:249
msgid "Edit template for direct messages. Current template: {}"
msgstr ""
msgstr "Direktnachrichten-Vorlage bearbeiten. Aktuelle Vorlage: {}"
#: controller/settings.py:231 wxUI/dialogs/configuration.py:251
msgid "Edit template for sent direct messages. Current template: {}"
msgstr ""
"Vorlage für gesendete Direktnachrichten bearbeiten. Aktuelle Vorlage: {}"
#: controller/settings.py:240 wxUI/dialogs/configuration.py:253
msgid "Edit template for persons. Current template: {}"
msgstr ""
msgstr "Vorlage für Personen bearbeiten. Aktuelle Vorlage: {}"
#: controller/settings.py:340
msgid "Direct Messages"
@@ -559,7 +544,7 @@ msgstr "Geschützt: %s\n"
#: controller/user.py:110
msgid "Relationship: "
msgstr ""
msgstr "Beziehung "
#: controller/user.py:112
msgid "You follow {0}. "
@@ -598,14 +583,12 @@ msgid "You can't ignore direct messages"
msgstr "Du kannst keine Direktnachrichten ignorieren."
#: controller/userAliasController.py:31
#, fuzzy
msgid "Edit alias for {}"
msgstr "Liste für {}"
msgstr "Alias für {} bearbeiten"
#: controller/userSelector.py:10
#, fuzzy
msgid "Select user"
msgstr "Wähle den Benutzer"
msgstr "Benutzer wählen"
#: controller/buffers/base/base.py:91 controller/buffers/mastodon/base.py:91
msgid "This action is not supported for this buffer"
@@ -677,9 +660,8 @@ msgid "New direct message"
msgstr "Neue Direktnachricht"
#: controller/buffers/twitter/base.py:452
#, fuzzy
msgid "This action is not supported on protected accounts."
msgstr "Diese Aktion ist in der Ansicht noch nicht verfügbar."
msgstr "Diese Aktion wird für geschützte Accounts nicht unterstützt."
#: controller/buffers/twitter/base.py:469
msgid "Quote"
@@ -1107,19 +1089,16 @@ msgid "Autocomplete users' settings"
msgstr "Auto-Vervollständigungs-Einstellungen"
#: extra/autocompletionUsers/wx_scan.py:11
#, fuzzy
msgid "Add followers to database"
msgstr "Benutzer zur Datenbank hinzufügen"
msgstr "Folger zur Datenbank hinzufügen"
#: extra/autocompletionUsers/wx_scan.py:12
#, fuzzy
msgid "Add friends to database"
msgstr "Benutzer zur Datenbank hinzufügen"
msgstr "Freunde zur Datenbank hinzufügen"
#: extra/autocompletionUsers/wx_scan.py:26
#, fuzzy
msgid "Updating autocompletion database"
msgstr "Auto-Vervollständigungsdatenbank verwalten"
msgstr "Auto-Vervollständigungsdatenbank aktualisieren"
#: extra/autocompletionUsers/wx_scan.py:37
msgid ""
@@ -1132,6 +1111,14 @@ msgid ""
"with no error, you will be redirected back to the account settings dialog. "
"Do you want to continue?"
msgstr ""
"Dieser Vorgang wird die von dir gewählten Benutzer auf Twitter abrufen und "
"sie in die Autovervollständigungs-Datenbank aufnehmen. Bitte beachte, dass "
"es hierbei zur Überschreitung von Twitters API-Begrenzungen kommen kann, "
"etwa bei sehr vielen Nutzern oder wenn dieser Vorgang innerhalb der letzten "
"15 Minuten bereits ausgeführt wurde. In diesem Fall wird eine Fehlermeldung "
"angezeigt und du kannst es in einigen Minuten erneut versuchen. Bei "
"erfolgreichem Abschluss wirst du in die Account-Einstellungen "
"weitergeleitet. Möchtest du fortfahren?"
#: extra/autocompletionUsers/wx_scan.py:37 wxUI/commonMessageDialogs.py:36
#: wxUI/commonMessageDialogs.py:86
@@ -1140,7 +1127,7 @@ msgstr "Achtung"
#: extra/autocompletionUsers/wx_scan.py:43
msgid "TWBlue has imported {} users successfully."
msgstr ""
msgstr "TWBlue hat die Benutzer erfolgreich importiert."
#: extra/autocompletionUsers/wx_scan.py:43
msgid "Done"
@@ -1149,6 +1136,8 @@ msgstr "Fertig"
#: extra/autocompletionUsers/wx_scan.py:47
msgid "Error adding users from Twitter. Please try again in about 15 minutes."
msgstr ""
"Fehler beim Hinzufügen der Twitter-Benutzer. Bitte warte 15 Minuten und "
"versuche es erneut."
#: extra/ocr/OCRSpace.py:7
msgid "Detect automatically"
@@ -1755,9 +1744,8 @@ msgid "Extracts the text from a picture and displays the result in a dialog."
msgstr "Extrahiert Text aus einem Bild und zeigt ihn in einem Dialog an."
#: keystrokeEditor/constants.py:59
#, fuzzy
msgid "Adds an alias to an user"
msgstr "Wähle eine Liste zum Hinzufügen des Benutzers."
msgstr "Erstellt einen Alias für einen Benutzer"
#: keystrokeEditor/wx_ui.py:8
msgid "Keystroke editor"
@@ -1782,9 +1770,8 @@ msgid "Edit"
msgstr "Bearbeiten"
#: keystrokeEditor/wx_ui.py:20 keystrokeEditor/wx_ui.py:50
#, fuzzy
msgid "Undefine keystroke"
msgstr "Bearbeite Tastenkombination"
msgstr "Tastenkombination entfernen"
#: keystrokeEditor/wx_ui.py:21
msgid "Execute action"
@@ -1797,12 +1784,11 @@ msgstr "Schließen"
#: keystrokeEditor/wx_ui.py:42
msgid "Undefined"
msgstr ""
msgstr "Nicht definiert"
#: keystrokeEditor/wx_ui.py:50
#, fuzzy
msgid "Are you sure you want to undefine this keystroke?"
msgstr "Möchtest du diese Liste wirklich löschen?"
msgstr "Möchtest du diese Tastenkombination wirklich entfernen?"
#: keystrokeEditor/wx_ui.py:54
msgid "Editing keystroke"
@@ -1918,10 +1904,15 @@ msgid ""
"after an account reactivation. Please remove the account manually from your "
"Twitter sessions in order to stop seeing this message."
msgstr ""
"TWBlue kann den Account für {} nicht in Twitter authentisieren. Dies kann "
"durch einen ungültigen oder veralteten Token ausgelöst werden, widerrufener "
"App-Zugriff oder nach einer Account-Reaktivierung. Bitte entferne den "
"Account manuell aus der Sitzungsverwaltung, um diesen Fehler nicht mehr "
"anzuzeigen."
#: sessionmanager/wxUI.py:81
msgid "Authentication error for session {}"
msgstr ""
msgstr "Authentisierungsfehler für Sitzung {}"
#: sessions/base.py:113
msgid ""
@@ -1929,6 +1920,9 @@ msgid ""
"and rebuilt automatically. If this error persists, send the error log to the "
"{app} developers."
msgstr ""
"Beim Speichern der {app}-Datenbank ist ein Ausnahmefehler aufgetreten. Sie "
"wird daher gelöscht und neu erstellt. Sollte das Problem weiterhin bestehen, "
"sende das Fehlerprotokoll an die {app}-Entwickler."
#: sessions/base.py:153
msgid ""
@@ -1936,6 +1930,9 @@ msgid ""
"and rebuilt automatically. If this error persists, send the error log to the "
"{app} developers."
msgstr ""
"Beim Laden der {app}-Datenbank ist ein Ausnahmefehler aufgetreten. Sie wird "
"daher gelöscht und neu erstellt. Sollte das Problem weiterhin bestehen, "
"sende das Fehlerprotokoll an die {app}-Entwickler."
#: sessions/twitter/compose.py:25 sessions/twitter/compose.py:68
#: sessions/twitter/compose.py:133 sessions/twitter/compose.py:142
@@ -1988,30 +1985,30 @@ msgid "%s succeeded."
msgstr "%s erfolgreich."
#: sessions/twitter/session.py:452 sessions/twitter/session.py:535
#, fuzzy
msgid "Deleted account"
msgstr "Neuer Account"
msgstr "Gelöschter Account"
#: sessions/twitter/templates.py:16
msgid "$display_name, $text $image_descriptions $date. $source"
msgstr ""
msgstr "$display_name, $text $image_descriptions $date. $source"
#: sessions/twitter/templates.py:17
msgid "$sender_display_name, $text $date"
msgstr ""
msgstr "$sender_display_name, $text $date"
#: sessions/twitter/templates.py:18
msgid "Dm to $recipient_display_name, $text $date"
msgstr ""
msgstr "Direktnachricht an $recipient_display_name, $text $date"
#: sessions/twitter/templates.py:19
msgid ""
"$display_name (@$screen_name). $followers followers, $following following, "
"$tweets tweets. Joined Twitter $created_at."
msgstr ""
"$display_name (@$screen_name). $followers Folger, folgt $following, $tweets "
"Tweets. Trat Twitter $created_at bei."
#: sessions/twitter/templates.py:54
#, fuzzy
msgid "Image description: {}."
msgstr "Bildbeschreibung"
@@ -2024,13 +2021,12 @@ msgid "No status found with that ID"
msgstr "Kein Status mit dieser ID gefunden."
#: sessions/twitter/utils.py:247
#, fuzzy
msgid "Error {0}"
msgstr "Fehlercode {0}"
msgstr "Fehler {0}"
#: sessions/twitter/utils.py:274
msgid "{user_1}, {user_2} and {all_users} more: {text}"
msgstr ""
msgstr "{user_1}, {user_2} und {all_users} weitere: {text}"
#: sessions/twitter/wxUI.py:7
msgid "Authorising account..."
@@ -2296,7 +2292,7 @@ msgstr ""
#: wxUI/commonMessageDialogs.py:95
msgid "The configuration file is invalid."
msgstr ""
msgstr "Die Konfigurationsdatei ist ungültig."
#: wxUI/commonMessageDialogs.py:98
msgid ""
@@ -2438,7 +2434,7 @@ msgstr "&Listen verwalten"
#: wxUI/view.py:23
msgid "Manage user aliases"
msgstr ""
msgstr "Benutzer-Aliase verwalten"
#: wxUI/view.py:24
msgid "&Edit keystrokes"
@@ -2478,7 +2474,7 @@ msgstr "&Direktnachricht"
#: wxUI/view.py:47
msgid "Add a&lias"
msgstr ""
msgstr "A&lias hinzufügen"
#: wxUI/view.py:48
msgid "&Add to list"
@@ -2510,7 +2506,7 @@ msgstr "Filter &verwalten"
#: wxUI/view.py:60
msgid "Find a string in the currently focused buffer..."
msgstr "Eine Zeichenkette im der momentan fokussierten Ansicht suchen..."
msgstr "Eine Zeichenkette in der momentan fokussierten Ansicht suchen..."
#: wxUI/view.py:61
msgid "&Load previous items"
@@ -2729,18 +2725,18 @@ msgid "Password: "
msgstr "Passwort: "
#: wxUI/dialogs/configuration.py:102
#, fuzzy
msgid "User autocompletion settings"
msgstr "Auto-Vervollständigungs-Einstellungen..."
msgstr "Auto-Vervollständigungs-Einstellungen"
#: wxUI/dialogs/configuration.py:103
msgid ""
"Scan account and add friends and followers to the user autocompletion "
"database"
msgstr ""
"Account scannen und Freunde und Folger zur Autovervollständigungs-Datenbank "
"hinzufügen"
#: wxUI/dialogs/configuration.py:104
#, fuzzy
msgid "Manage autocompletion database"
msgstr "Auto-Vervollständigungsdatenbank verwalten"
@@ -2770,7 +2766,7 @@ msgstr "Zeige Benutzernamen statt der vollständigen Namen"
#: wxUI/dialogs/configuration.py:128
msgid "hide emojis in usernames"
msgstr ""
msgstr "Emojis in Benutzernamen ausblenden"
#: wxUI/dialogs/configuration.py:130
msgid ""
@@ -2785,6 +2781,8 @@ msgid ""
"Load cache for tweets in memory (much faster in big datasets but requires "
"more RAM)"
msgstr ""
"Tweets im Speicher zwischenlagern (sorgt für verbesserte Geschwindigkeit bei "
"großen Datenmengen, erhöht jedoch die RAM-Nutzung)"
#: wxUI/dialogs/configuration.py:141
msgid "Enable automatic speech feedback"
@@ -2910,7 +2908,7 @@ msgstr "Ansichten"
#: wxUI/dialogs/configuration.py:406
msgid "Templates"
msgstr ""
msgstr "Vorlagen"
#: wxUI/dialogs/configuration.py:410
msgid "Sound"
@@ -3004,11 +3002,11 @@ msgstr "Abbrechen"
#: wxUI/dialogs/filterDialogs.py:120
msgid "You must define a name for the filter before creating it."
msgstr ""
msgstr "Du musst vor dem Erstellen eines Filters einen namen festlegen."
#: wxUI/dialogs/filterDialogs.py:120
msgid "Missing filter name"
msgstr ""
msgstr "Fehlender Filtername"
#: wxUI/dialogs/filterDialogs.py:127
msgid "Manage filters"
@@ -3269,53 +3267,44 @@ msgid "&Ignore tweets from this client"
msgstr "Tweets dieses Clients &ignorieren"
#: wxUI/dialogs/userAliasDialogs.py:18
#, fuzzy
msgid "Alias"
msgstr "immer"
msgstr "Alias"
#: wxUI/dialogs/userAliasDialogs.py:41
#, fuzzy
msgid "Edit user aliases"
msgstr "Bearbeiten der {0} Benutzerdatenbank"
msgstr "Benutzer-Aliase bearbeiten"
#: wxUI/dialogs/userAliasDialogs.py:48
#, fuzzy
msgid "Actions"
msgstr "Aktion"
msgstr "Aktionen"
#: wxUI/dialogs/userAliasDialogs.py:50
#, fuzzy
msgid "Add alias"
msgstr "Zur Liste hinzufügen"
msgstr "Alias hinzufügen"
#: wxUI/dialogs/userAliasDialogs.py:51
msgid "Adds a new user alias"
msgstr ""
msgstr "Fügt einen neuen Benutzer-Alias hinzu"
#: wxUI/dialogs/userAliasDialogs.py:54
#, fuzzy
msgid "Edit the currently focused user Alias."
msgstr "Interagiert mit dem momentan fokussierten Tweet."
msgstr "Den momentan fokussierten Benutzer-Alias bearbeiten."
#: wxUI/dialogs/userAliasDialogs.py:58
#, fuzzy
msgid "Remove the currently focused user alias."
msgstr "Eine Zeichenkette im der momentan fokussierten Ansicht suchen..."
msgstr "Den momentan gewählten Benutzer-Alias entfernen."
#: wxUI/dialogs/userAliasDialogs.py:82
#, fuzzy
msgid "Are you sure you want to delete this user alias?"
msgstr "Möchtest du diese Liste wirklich löschen?"
msgstr "Möchtest du diesen Benutzer-Alias wirklich löschen?"
#: wxUI/dialogs/userAliasDialogs.py:82
#, fuzzy
msgid "Remove user alias"
msgstr "Benutzer entfernen"
msgstr "Benutzer-Alias entfernen"
#: wxUI/dialogs/userAliasDialogs.py:93
#, fuzzy
msgid "User alias"
msgstr "Benutzerdetails"
msgstr "Benutzer-Alias"
#: wxUI/dialogs/userSelection.py:10
#, python-format
@@ -3344,24 +3333,23 @@ msgstr "F&reunde"
#: wxUI/dialogs/twitterDialogs/templateDialogs.py:8
msgid "Edit Template"
msgstr ""
msgstr "Vorlage bearbeiten"
#: wxUI/dialogs/twitterDialogs/templateDialogs.py:13
msgid "Edit template"
msgstr ""
msgstr "Vorlage bearbeiten"
#: wxUI/dialogs/twitterDialogs/templateDialogs.py:17
#, fuzzy
msgid "Available variables"
msgstr "Nicht verfügbar"
msgstr "Verfügbare Variablen"
#: wxUI/dialogs/twitterDialogs/templateDialogs.py:29
msgid "Restore template"
msgstr ""
msgstr "Vorlage wiederherstellen"
#: wxUI/dialogs/twitterDialogs/templateDialogs.py:48
msgid "Restored template to {}."
msgstr ""
msgstr "Vorlage zu {} wiederhergestellt."
#: wxUI/dialogs/twitterDialogs/templateDialogs.py:52
msgid ""
@@ -3370,11 +3358,14 @@ msgid ""
"see a list of all available variables in the variables list while editing "
"your template."
msgstr ""
"Die von dir spezifizierte Vorlage enthält Variablen, die für dieses Objekt "
"nicht verfügbar sind. Bitte korrigiere die Vorlage und versuche es erneut. "
"Eine Liste der verfügbaren Variablen wird im Bearbeitungsdialog für die "
"Vorlage angezeigt."
#: wxUI/dialogs/twitterDialogs/templateDialogs.py:52
#, fuzzy
msgid "Invalid template"
msgstr "Ungültige Tastenkombination"
msgstr "Ungültige Vorlage"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:32
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:48
@@ -3392,30 +3383,26 @@ msgstr "Typ"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:39
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:175
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:259
#, fuzzy
msgid "Delete attachment"
msgstr "Anhang entfernen"
msgstr "Anhang löschen"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:44
#, fuzzy
msgid "Added Tweets"
msgstr "Gesendete Tweets"
msgstr "Hinzugefügte Tweets"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:51
#, fuzzy
msgid "Delete tweet"
msgstr "Gesendete Tweets"
msgstr "Tweet löschen"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:56
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:190
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:264
msgid "A&dd..."
msgstr ""
msgstr "Hin&zufügen..."
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:58
#, fuzzy
msgid "Add t&weet"
msgstr "Für einen Tweet \"gefällt mir\" abgeben"
msgstr "T&weet hinzufügen"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:61
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:192
@@ -3440,9 +3427,8 @@ msgstr "Rechtschreib&prüfung..."
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:69
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:200
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:272
#, fuzzy
msgid "&Translate"
msgstr "Übersetzt"
msgstr "Überse&tzen"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:73
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:204
@@ -3454,31 +3440,29 @@ msgstr "Sen&den"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:218
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:296
msgid "Image"
msgstr ""
msgstr "Bild"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:119
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:220
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:298
#, fuzzy
msgid "Video"
msgstr "Verbergen"
msgstr "Video"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:121
msgid "Poll"
msgstr ""
msgstr "Umfrage"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:126
msgid "please provide a description"
msgstr "Bitte gib eine Beschreibung ein"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:140
#, fuzzy
msgid "Select the video to be uploaded"
msgstr "Wähle das hochzuladende Bild aus."
msgstr "Wähle das hochzuladende Video aus."
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:140
msgid "Video files (*.mp4)|*.mp4"
msgstr ""
msgstr "Videodateien (*.mp4)|*.mp4"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:146
msgid ""
@@ -3486,11 +3470,13 @@ msgid ""
"complies with Twitter'S attachment rules. You can add only one video or GIF "
"in every tweet, and a maximum of 4 photos."
msgstr ""
"Weitere Anhänge können nicht hinzugefügt werden. Bitte stelle sicher, dass "
"dein Tweet den Twitter-Regeln für Anhänge entspricht. Du kannst nur ein "
"Video oder Gif pro Tweet senden, und maximal 4 Fotos."
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:146
#, fuzzy
msgid "Error adding attachment"
msgstr "Einen Anhang hinzufügen"
msgstr "Fehler beim Hinzufügen des Anhangs"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:180
msgid "&Mention to all"
@@ -3528,9 +3514,8 @@ msgstr "Datum"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:362
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:435
#, fuzzy
msgid "Copy link to clipboard"
msgstr "In Zwischenablage kopieren"
msgstr "Link in Zwischenablage kopieren"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:365
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:440
@@ -3556,37 +3541,38 @@ msgstr "URL &expandieren"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:477
msgid "Add a poll"
msgstr ""
msgstr "Füge eine Umfrage hinzu"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:481
msgid "Participation time (in days)"
msgstr ""
msgstr "Teilnahmezeitraum (in Tagen)"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:488
msgid "Choices"
msgstr ""
msgstr "Wahlmöglichkeiten"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:492
msgid "Option 1"
msgstr ""
msgstr "Option 1"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:499
msgid "Option 2"
msgstr ""
msgstr "Option 2"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:506
msgid "Option 3"
msgstr ""
msgstr "Option 3"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:513
msgid "Option 4"
msgstr ""
msgstr "Option 4"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:541
msgid "Please make sure you have provided at least two options for the poll."
msgstr ""
"Bitte stelle sicher, dass mindestens zwei Optionen für die Umfrage angegeben "
"wurden."
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:541
#, fuzzy
msgid "Not enough information"
msgstr "Information"
msgstr "Nicht genügend Informationen"

Binary file not shown.

View File

@@ -1,107 +1,92 @@
# Corentin Bacqué-Cazenave <corentin@progaccess.net>, 2022.
msgid ""
msgstr ""
"Project-Id-Version: TW Blue 0.94\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Report-Msgid-Bugs-To: manuel@manuelcortez.net\n"
"POT-Creation-Date: 2022-08-16 17:50-0500\n"
"PO-Revision-Date: 2022-08-18 15:32+0200\n"
"PO-Revision-Date: 2022-08-29 17:03+0000\n"
"Last-Translator: Corentin Bacqué-Cazenave <corentin@progaccess.net>\n"
"Language-Team: Corentin Bacqué-Cazenave <corentin@progaccess.net>\n"
"Language-Team: French <https://weblate.mcvsoftware.com/projects/twblue/"
"twblue/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Weblate 4.13.1\n"
"Generated-By: Babel 2.10.3\n"
"X-Generator: Poedit 3.1.1\n"
#: languageHandler.py:61
#, fuzzy
msgctxt "languageName"
msgid "Amharic"
msgstr "Amharique"
#: languageHandler.py:62
#, fuzzy
msgctxt "languageName"
msgid "Aragonese"
msgstr "Japonais"
#: languageHandler.py:63
#, fuzzy
msgctxt "languageName"
msgid "Spanish"
msgstr "Espagnol"
#: languageHandler.py:64
#, fuzzy
msgctxt "languageName"
msgid "Portuguese"
msgstr "Portugais"
#: languageHandler.py:65
#, fuzzy
msgctxt "languageName"
msgid "Russian"
msgstr "Russe"
#: languageHandler.py:66
#, fuzzy
msgctxt "languageName"
msgid "italian"
msgstr "Italien"
#: languageHandler.py:67
#, fuzzy
#| msgid "Turkish"
msgctxt "languageName"
msgid "Turkey"
msgstr "Turc"
#: languageHandler.py:68
#, fuzzy
msgctxt "languageName"
msgid "Galician"
msgstr "Galicien"
#: languageHandler.py:69
#, fuzzy
msgctxt "languageName"
msgid "Catala"
msgstr "Catalan"
#: languageHandler.py:70
#, fuzzy
msgctxt "languageName"
msgid "Vasque"
msgstr "Basque"
#: languageHandler.py:71
#, fuzzy
msgctxt "languageName"
msgid "polish"
msgstr "Polonais"
#: languageHandler.py:72
#, fuzzy
msgctxt "languageName"
msgid "Arabic"
msgstr "Arabe"
#: languageHandler.py:73
#, fuzzy
msgctxt "languageName"
msgid "Nepali"
msgstr "Népali"
msgstr "Népalais"
#: languageHandler.py:74
#, fuzzy
#| msgid "Serbian"
msgctxt "languageName"
msgid "Serbian (Latin)"
msgstr "Serbe"
#: languageHandler.py:75
#, fuzzy
msgctxt "languageName"
msgid "Japanese"
msgstr "Japonais"
@@ -205,7 +190,6 @@ msgstr "Favoris de {}"
# | msgid "Followers timelines"
#: controller/mainController.py:364
#, fuzzy
msgid "Followers timelines"
msgstr "Chronologies des abonnés"
@@ -602,7 +586,6 @@ msgid "Edit alias for {}"
msgstr "Éditer l'alias pour {}"
#: controller/userSelector.py:10
#, fuzzy
msgid "Select user"
msgstr "Sélectionnez l'utilisateur"
@@ -1106,19 +1089,16 @@ msgid "Autocomplete users' settings"
msgstr "Paramètres pour la saisie automatique des utilisateurs"
#: extra/autocompletionUsers/wx_scan.py:11
#, fuzzy
msgid "Add followers to database"
msgstr "Ajouter l'utilisateur à la base de données"
#: extra/autocompletionUsers/wx_scan.py:12
#, fuzzy
msgid "Add friends to database"
msgstr "Ajouter l'utilisateur à la base de données"
msgstr "Ajouter les abonnements à la base de données"
#: extra/autocompletionUsers/wx_scan.py:26
#, fuzzy
msgid "Updating autocompletion database"
msgstr "Gérer la base de données pour la saisie automatique"
msgstr "Mise à jour de la base de données d'autocomplétion"
#: extra/autocompletionUsers/wx_scan.py:37
msgid ""
@@ -2044,7 +2024,6 @@ msgstr "Aucun Tweet trouvée avec cet ID"
# | msgid "Error {0}"
#: sessions/twitter/utils.py:247
#, fuzzy
msgid "Error {0}"
msgstr "Erreur {0}"
@@ -2757,9 +2736,8 @@ msgid "Password: "
msgstr "Mot de passe: "
#: wxUI/dialogs/configuration.py:102
#, fuzzy
msgid "User autocompletion settings"
msgstr "Paramètres pour la saisie automatique..."
msgstr "Paramètres d'autocomplétion utilisateur"
#: wxUI/dialogs/configuration.py:103
msgid ""
@@ -2770,9 +2748,8 @@ msgstr ""
"données d'autocomplétion des utilisateurs."
#: wxUI/dialogs/configuration.py:104
#, fuzzy
msgid "Manage autocompletion database"
msgstr "Gérer la base de données pour la saisie automatique"
msgstr "Gérer la base de données d'autocomplétion"
#: wxUI/dialogs/configuration.py:109
msgid "Relative timestamps"
@@ -2799,7 +2776,6 @@ msgid "Show screen names instead of full names"
msgstr "Afficher les noms d'écran au lieu des noms complets"
#: wxUI/dialogs/configuration.py:128
#, fuzzy
msgid "hide emojis in usernames"
msgstr "Masquer les emojis dans les noms d'utilisateur"
@@ -3419,7 +3395,6 @@ msgstr "Type"
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:39
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:175
#: wxUI/dialogs/twitterDialogs/tweetDialogs.py:259
#, fuzzy
msgid "Delete attachment"
msgstr "Supprimer la pièce jointe"

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: TWBlue 0.80\n"
"Report-Msgid-Bugs-To: manuel@manuelcortez.net\n"
"POT-Creation-Date: 2022-08-16 17:50-0500\n"
"PO-Revision-Date: 2022-08-25 18:09+0000\n"
"PO-Revision-Date: 2022-08-29 17:03+0000\n"
"Last-Translator: zvonimir stanecic <zvonimirek222@yandex.com>\n"
"Language-Team: Croatian <https://weblate.mcvsoftware.com/projects/twblue/"
"twblue/hr/>\n"
@@ -419,12 +419,10 @@ msgid "System default"
msgstr "Korisnićki zadan"
#: controller/settings.py:77
#, fuzzy
msgid "HTTP"
msgstr "HTTP"
#: controller/settings.py:77
#, fuzzy
msgid "SOCKS v4"
msgstr "SOCKS v4"
@@ -433,7 +431,6 @@ msgid "SOCKS v4 with DNS support"
msgstr "SOCKS v4 sa DNS podrškom"
#: controller/settings.py:77
#, fuzzy
msgid "SOCKS v5"
msgstr "SOCKS v5"
@@ -1930,6 +1927,9 @@ msgid ""
"and rebuilt automatically. If this error persists, send the error log to the "
"{app} developers."
msgstr ""
"Dogogila se pogreška prilikom izgradnje {app} baze podataka. Ista će biti "
"izbrisana i ponovno izgrađena. Ako se ova pogreška ponavlja, pošaljite "
"zapisnik o pogrešci {app} programerima."
#: sessions/twitter/compose.py:25 sessions/twitter/compose.py:68
#: sessions/twitter/compose.py:133 sessions/twitter/compose.py:142
@@ -1982,32 +1982,34 @@ msgid "%s succeeded."
msgstr "%s uspjelo."
#: sessions/twitter/session.py:452 sessions/twitter/session.py:535
#, fuzzy
msgid "Deleted account"
msgstr "Novi nalog"
msgstr "Izbrisani račun"
#: sessions/twitter/templates.py:16
#, fuzzy
msgid "$display_name, $text $image_descriptions $date. $source"
msgstr ""
msgstr "$display_name, $text $image_descriptions $date. $source"
#: sessions/twitter/templates.py:17
#, fuzzy
msgid "$sender_display_name, $text $date"
msgstr ""
msgstr "$sender_display_name, $text $date"
#: sessions/twitter/templates.py:18
msgid "Dm to $recipient_display_name, $text $date"
msgstr ""
msgstr "Dm do $recipient_display_name, $text $date"
#: sessions/twitter/templates.py:19
msgid ""
"$display_name (@$screen_name). $followers followers, $following following, "
"$tweets tweets. Joined Twitter $created_at."
msgstr ""
"$display_name (@$screen_name). $followers pratitelja, $following praćenih, $"
"tweets tweetova. Pridružio se twitteru $created_at."
#: sessions/twitter/templates.py:54
#, fuzzy
msgid "Image description: {}."
msgstr "opis slike"
msgstr "Opis slike: {}."
#: sessions/twitter/utils.py:243
msgid "Sorry, you are not authorised to see this status."
@@ -2018,13 +2020,12 @@ msgid "No status found with that ID"
msgstr "Nema pronađenog statusa sa tim identifikatorom"
#: sessions/twitter/utils.py:247
#, fuzzy
msgid "Error {0}"
msgstr "Kod pogreške {0}"
msgstr "pogreška {0}"
#: sessions/twitter/utils.py:274
msgid "{user_1}, {user_2} and {all_users} more: {text}"
msgstr ""
msgstr "{user_1}, {user_2} i {all_users} više: {text}"
#: sessions/twitter/wxUI.py:7
msgid "Authorising account..."
@@ -2280,7 +2281,7 @@ msgstr "Ovaj filter već postoji. Koristite drugi naslov"
#: wxUI/commonMessageDialogs.py:95
msgid "The configuration file is invalid."
msgstr ""
msgstr "Konfiguracijska datoteka je neispravna."
#: wxUI/commonMessageDialogs.py:98
msgid ""
@@ -2422,7 +2423,7 @@ msgstr "&Upravitelj listi"
#: wxUI/view.py:23
msgid "Manage user aliases"
msgstr ""
msgstr "Upravljanje korisničkim aliasima"
#: wxUI/view.py:24
msgid "&Edit keystrokes"
@@ -2462,7 +2463,7 @@ msgstr "Izravna po&ruka"
#: wxUI/view.py:47
msgid "Add a&lias"
msgstr ""
msgstr "dodaj a&lias"
#: wxUI/view.py:48
msgid "&Add to list"
@@ -2711,20 +2712,20 @@ msgid "Password: "
msgstr "Lozinka"
#: wxUI/dialogs/configuration.py:102
#, fuzzy
msgid "User autocompletion settings"
msgstr "Postavke korisnika koji se samodovršuju"
msgstr "Postavke korisnika koji se samodovršavaju"
#: wxUI/dialogs/configuration.py:103
msgid ""
"Scan account and add friends and followers to the user autocompletion "
"database"
msgstr ""
"Skeniraj račun i dodaj prijatelje i pratitelje u bazu podataka "
"samodovršavanja"
#: wxUI/dialogs/configuration.py:104
#, fuzzy
msgid "Manage autocompletion database"
msgstr "upravljaj bazom podataka korisnika koji se samodovršavaju"
msgstr "Upravljaj bazom podataka samodovršavanja"
#: wxUI/dialogs/configuration.py:109
msgid "Relative timestamps"
@@ -2752,7 +2753,7 @@ msgstr "Prikazuj prikazana imena umjesto punih"
#: wxUI/dialogs/configuration.py:128
msgid "hide emojis in usernames"
msgstr ""
msgstr "Skrivaj emojie u korisničkim imenima"
#: wxUI/dialogs/configuration.py:130
msgid ""
@@ -2767,6 +2768,8 @@ msgid ""
"Load cache for tweets in memory (much faster in big datasets but requires "
"more RAM)"
msgstr ""
"Učitaj predmemoriju tweetowa u memoriju (brže u velikim setovima podataka "
"ali zahtjeva više radne memorije)"
#: wxUI/dialogs/configuration.py:141
msgid "Enable automatic speech feedback"
@@ -2892,7 +2895,7 @@ msgstr "&Spremnik"
#: wxUI/dialogs/configuration.py:406
msgid "Templates"
msgstr ""
msgstr "Predlošci"
#: wxUI/dialogs/configuration.py:410
msgid "Sound"
@@ -2986,11 +2989,11 @@ msgstr "Odustani"
#: wxUI/dialogs/filterDialogs.py:120
msgid "You must define a name for the filter before creating it."
msgstr ""
msgstr "Prije stvaranja filtra, morate definirati njegov naziv."
#: wxUI/dialogs/filterDialogs.py:120
msgid "Missing filter name"
msgstr ""
msgstr "Naziv filtra koji nedostaje"
#: wxUI/dialogs/filterDialogs.py:127
msgid "Manage filters"
@@ -3251,38 +3254,32 @@ msgid "&Ignore tweets from this client"
msgstr "&Zanemari tweetove od ovog klijenta"
#: wxUI/dialogs/userAliasDialogs.py:18
#, fuzzy
msgid "Alias"
msgstr "uvijek"
msgstr "Alias"
#: wxUI/dialogs/userAliasDialogs.py:41
#, fuzzy
msgid "Edit user aliases"
msgstr "Uređujem {0} bazu podataka korisnika"
msgstr "Uredi aliase korisnika"
#: wxUI/dialogs/userAliasDialogs.py:48
#, fuzzy
msgid "Actions"
msgstr "Akcija"
msgstr "Radnje"
#: wxUI/dialogs/userAliasDialogs.py:50
#, fuzzy
msgid "Add alias"
msgstr "Dodaj u popis"
msgstr "Dodaj alias"
#: wxUI/dialogs/userAliasDialogs.py:51
msgid "Adds a new user alias"
msgstr ""
msgstr "Dodaje novi korisnički alias"
#: wxUI/dialogs/userAliasDialogs.py:54
#, fuzzy
msgid "Edit the currently focused user Alias."
msgstr "Uđi u interakciju sa trenutnim tweetom"
msgstr "Uredi trenutno fokusiran korisnički alias."
#: wxUI/dialogs/userAliasDialogs.py:58
#, fuzzy
msgid "Remove the currently focused user alias."
msgstr "Potraži niz znakova u trenutno fokusiranom spremniku..."
msgstr "Ukloni trenutno fokusiran korisnički alias."
#: wxUI/dialogs/userAliasDialogs.py:82
#, fuzzy

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,90 +1,80 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2022 ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
# Jonas S. Marques <jonasivle@gmail.com>, 2022.
msgid ""
msgstr ""
"Project-Id-Version: TWBlue 0.80\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Report-Msgid-Bugs-To: manuel@manuelcortez.net\n"
"POT-Creation-Date: 2022-08-16 17:50-0500\n"
"PO-Revision-Date: 2022-02-25 07:19-0300\n"
"Last-Translator: Manuel Cortez <manuel@manuelcortez.net>\n"
"Language-Team: Odenilton Júnior Santos <odeniltonjunior@gmail.com>\n"
"Language: pt_BR\n"
"PO-Revision-Date: 2022-09-04 00:02+0000\n"
"Last-Translator: Jonas S. Marques <jonasivle@gmail.com>\n"
"Language-Team: Portuguese <https://weblate.mcvsoftware.com/projects/twblue/"
"twblue/pt/>\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.13.1\n"
"Generated-By: Babel 2.10.3\n"
#: languageHandler.py:61
#, fuzzy
msgctxt "languageName"
msgid "Amharic"
msgstr "Dari"
#: languageHandler.py:62
#, fuzzy
msgctxt "languageName"
msgid "Aragonese"
msgstr "Japonês"
#: languageHandler.py:63
#, fuzzy
msgctxt "languageName"
msgid "Spanish"
msgstr "Espanhol"
#: languageHandler.py:64
#, fuzzy
msgctxt "languageName"
msgid "Portuguese"
msgstr "Português"
#: languageHandler.py:65
#, fuzzy
msgctxt "languageName"
msgid "Russian"
msgstr "Russo"
#: languageHandler.py:66
#, fuzzy
msgctxt "languageName"
msgid "italian"
msgstr "Italiano"
#: languageHandler.py:67
#, fuzzy
msgctxt "languageName"
msgid "Turkey"
msgstr "recurso"
msgstr "turco"
#: languageHandler.py:68
#, fuzzy
msgctxt "languageName"
msgid "Galician"
msgstr "Galego"
#: languageHandler.py:69
#, fuzzy
msgctxt "languageName"
msgid "Catala"
msgstr "Catalão"
#: languageHandler.py:70
#, fuzzy
msgctxt "languageName"
msgid "Vasque"
msgstr "Basco"
#: languageHandler.py:71
#, fuzzy
msgctxt "languageName"
msgid "polish"
msgstr "Polonês"
#: languageHandler.py:72
#, fuzzy
msgctxt "languageName"
msgid "Arabic"
msgstr "Árabe"
@@ -93,15 +83,14 @@ msgstr "Árabe"
#, fuzzy
msgctxt "languageName"
msgid "Nepali"
msgstr "Nepali"
msgstr "Nepalês"
#: languageHandler.py:74
msgctxt "languageName"
msgid "Serbian (Latin)"
msgstr ""
msgstr "Sérvio (Latin)"
#: languageHandler.py:75
#, fuzzy
msgctxt "languageName"
msgid "Japanese"
msgstr "Japonês"
@@ -172,9 +161,8 @@ msgid "Followers"
msgstr "Seguidores"
#: controller/mainController.py:351
#, fuzzy
msgid "Following"
msgstr "&Deixar de seguir"
msgstr "Seguindo"
#: controller/buffers/twitter/base.py:70 controller/mainController.py:353
#: controller/mainController.py:1399 controller/settings.py:346
@@ -205,7 +193,6 @@ msgid "Likes for {}"
msgstr "Curtidas de {}"
#: controller/mainController.py:364
#, fuzzy
msgid "Followers timelines"
msgstr "Linhas do tempo de Seguidores"
@@ -215,14 +202,13 @@ msgid "Followers for {}"
msgstr "Seguidores de {}"
#: controller/mainController.py:368
#, fuzzy
msgid "Following timelines"
msgstr "Linhas do tempo de Seguidores"
msgstr "Linhas do tempo de amigos"
#: controller/mainController.py:371 controller/mainController.py:934
#: controller/mainController.py:1582
msgid "Friends for {}"
msgstr "Seguidos por {}"
msgstr "Pessoas que {} segue"
#: controller/mainController.py:372 wxUI/dialogs/lists.py:13
msgid "Lists"
@@ -271,15 +257,15 @@ msgstr "Filtros não são suportados neste exibidor"
#: controller/mainController.py:747
msgid "Add an user alias"
msgstr ""
msgstr "Adicionar um atalho de usuário"
#: controller/mainController.py:755
msgid "Alias has been set correctly for {}."
msgstr ""
msgstr "Sucesso ao definir um atalho para {}"
#: controller/mainController.py:823 controller/messages.py:328
msgid "MMM D, YYYY. H:m"
msgstr "dddd, D MMMM, YYYY. H:m:s"
msgstr "MMM D, YYYY. H:m"
#: controller/mainController.py:951
msgid "Conversation with {0}"
@@ -295,7 +281,7 @@ msgstr "Erro ao decodificar coordenadas. Tente novamente mais tarde."
#: controller/mainController.py:998
msgid "Unable to find address in OpenStreetMap."
msgstr ""
msgstr "Não foi possível encontrar este endereço nos mapas do OpenStreet."
#: controller/mainController.py:1011
msgid "There are no results for the coordinates in this tweet"
@@ -422,37 +408,35 @@ msgstr "Tweet"
#: controller/messages.py:355
msgid "View item"
msgstr "Ver item"
msgstr "Visualizar item"
#: controller/messages.py:381
#, fuzzy
msgid "Link copied to clipboard."
msgstr "Copiar para área de transferência"
#: controller/settings.py:77
#, fuzzy
msgid "System default"
msgstr "Padrão do usuário"
msgstr "Padrão do sistema"
#: controller/settings.py:77
msgid "HTTP"
msgstr ""
msgstr "http"
#: controller/settings.py:77
msgid "SOCKS v4"
msgstr ""
msgstr "SOCKS v4"
#: controller/settings.py:77
msgid "SOCKS v4 with DNS support"
msgstr ""
msgstr "SOCKS v4 Com suporte à dns)"
#: controller/settings.py:77
msgid "SOCKS v5"
msgstr ""
msgstr "SOCKS v5"
#: controller/settings.py:77
msgid "SOCKS v5 with DNS support"
msgstr ""
msgstr "SOCKS v5 (Com suporte à dns)"
#: controller/settings.py:155 controller/settings.py:269
#: wxUI/dialogs/configuration.py:121
@@ -475,19 +459,19 @@ msgstr "Configurações de conta para %s"
#: controller/settings.py:213 wxUI/dialogs/configuration.py:247
msgid "Edit template for tweets. Current template: {}"
msgstr ""
msgstr "Editar modelo de tweets: Modelo atual {}"
#: controller/settings.py:222 wxUI/dialogs/configuration.py:249
msgid "Edit template for direct messages. Current template: {}"
msgstr ""
msgstr "Editar modelo de mensagens diretas. Modelo atual {}"
#: controller/settings.py:231 wxUI/dialogs/configuration.py:251
msgid "Edit template for sent direct messages. Current template: {}"
msgstr ""
msgstr "Editar modelo para mensagens diretas enviadas. Modelo atual {}"
#: controller/settings.py:240 wxUI/dialogs/configuration.py:253
msgid "Edit template for persons. Current template: {}"
msgstr ""
msgstr "Editar modelos para pessoas. Modelo atual {}"
#: controller/settings.py:340
msgid "Direct Messages"
@@ -561,7 +545,7 @@ msgstr "Protegido: %s\n"
#: controller/user.py:110
msgid "Relationship: "
msgstr ""
msgstr "Relação entre vocês:· "
#: controller/user.py:112
msgid "You follow {0}. "
@@ -600,12 +584,10 @@ msgid "You can't ignore direct messages"
msgstr "Você não pode ignorar mensagens diretas"
#: controller/userAliasController.py:31
#, fuzzy
msgid "Edit alias for {}"
msgstr "Lista de {}"
msgstr "Editar atalho de {}"
#: controller/userSelector.py:10
#, fuzzy
msgid "Select user"
msgstr "Selecione o usuário"
@@ -615,11 +597,11 @@ msgstr "Esta ação não é suportada neste exibidor"
#: controller/buffers/twitter/base.py:76
msgid "{username}'s timeline"
msgstr "Abrir linha do tempo do usuário"
msgstr "Linha do tempo de {username}'s"
#: controller/buffers/twitter/base.py:78
msgid "{username}'s likes"
msgstr "Curtidas dos {username}'s "
msgstr "curtidas de {username}'s"
#: controller/buffers/twitter/base.py:80
msgid "{username}'s followers"
@@ -627,7 +609,7 @@ msgstr "{username}'s seguidores"
#: controller/buffers/twitter/base.py:82
msgid "{username}'s friends"
msgstr "{nome de usuário dos amigos}"
msgstr "Amigos de {username}'s"
#: controller/buffers/twitter/base.py:84
msgid "Unknown buffer"

View File

@@ -1,22 +1,16 @@
# -*- coding: utf-8 -*-
import platform
from win32com.client import GetObject
""" there are lots of things not implemented for Gtk+ yet.
We've started this effort 1 Apr 2015 so it isn't fully functional. We will remove the ifs statements when are no needed"""
system = platform.system()
if system == "Windows":
import sys
import os
#redirect the original stdout and stderr
stdout=sys.stdout
stderr=sys.stderr
sys.stdout = open(os.path.join(os.getenv("temp"), "stdout.log"), "w")
sys.stderr = open(os.path.join(os.getenv("temp"), "stderr.log"), "w")
import sys
import os
import platform
#redirect the original stdout and stderr
stdout=sys.stdout
stderr=sys.stderr
sys.stdout = open(os.path.join(os.getenv("temp"), "stdout.log"), "w")
sys.stderr = open(os.path.join(os.getenv("temp"), "stderr.log"), "w")
import languageHandler
import paths
#check if TWBlue is installed (Windows only)
#check if TWBlue is installed
# ToDo: Remove this soon as this is done already when importing the paths module.
if os.path.exists(os.path.join(paths.app_path(), "Uninstall.exe")):
paths.mode="installed"
@@ -31,34 +25,30 @@ import fixes
import widgetUtils
import webbrowser
from wxUI import commonMessageDialogs
if system == "Windows":
from logger import logger
from update import updater
stdout_temp=sys.stdout
stderr_temp=sys.stderr
from logger import logger
from update import updater
stdout_temp=sys.stdout
stderr_temp=sys.stderr
#if it's a binary version
if hasattr(sys, 'frozen'):
sys.stderr = open(os.path.join(paths.logs_path(), "stderr.log"), 'w')
sys.stdout = open(os.path.join(paths.logs_path(), "stdout.log"), 'w')
else:
sys.stdout=stdout
sys.stderr=stderr
# We are running from source, let's prepare vlc module for that situation
if system=="Windows":
arch="x86"
if platform.architecture()[0][:2] == "64":
arch="x64"
os.environ['PYTHON_VLC_MODULE_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", arch))
os.environ['PYTHON_VLC_LIB_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", arch, "libvlc.dll"))
#the final log files have been opened succesfully, let's close the temporary files
stdout_temp.close()
stderr_temp.close()
#finally, remove the temporary files. TW Blue doesn't need them anymore, and we will get more free space on the harddrive
os.remove(stdout_temp.name)
os.remove(stderr_temp.name)
if hasattr(sys, 'frozen'):
sys.stderr = open(os.path.join(paths.logs_path(), "stderr.log"), 'w')
sys.stdout = open(os.path.join(paths.logs_path(), "stdout.log"), 'w')
else:
sys.stdout=stdout
sys.stderr=stderr
# We are running from source, let's prepare vlc module for that situation
arch="x86"
if platform.architecture()[0][:2] == "64":
arch="x64"
os.environ['PYTHON_VLC_MODULE_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", arch))
os.environ['PYTHON_VLC_LIB_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", arch, "libvlc.dll"))
#the final log files have been opened succesfully, let's close the temporary files
stdout_temp.close()
stderr_temp.close()
#finally, remove the temporary files. TW Blue doesn't need them anymore, and we will get more free space on the harddrive
os.remove(stdout_temp.name)
os.remove(stderr_temp.name)
import sound
if system == "Linux":
from gi.repository import Gdk, GObject, GLib
log = logging.getLogger("main")
@@ -79,14 +69,14 @@ def setup():
from sessionmanager import sessionManager
app = widgetUtils.mainLoopObject()
check_pid()
if system == "Windows":
if config.app["app-settings"]["donation_dialog_displayed"] == False:
donation()
if config.app['app-settings']['check_for_updates']:
updater.do_update()
if config.app["app-settings"]["donation_dialog_displayed"] == False:
donation()
if config.app['app-settings']['check_for_updates']:
updater.do_update()
sm = sessionManager.sessionManagerController()
sm.fill_list()
if len(sm.sessions) == 0: sm.show()
if len(sm.sessions) == 0:
sm.show()
else:
sm.do_ok()
if hasattr(sm.view, "destroy"):
@@ -96,10 +86,7 @@ def setup():
r.view.show()
r.do_work()
r.check_invisible_at_startup()
if system == "Windows":
call_threaded(r.start)
elif system == "Linux":
GLib.idle_add(r.start)
call_threaded(r.start)
app.run()
def proxy_setup():

53
src/mastodon.defaults Normal file
View File

@@ -0,0 +1,53 @@
[mastodon]
access_token = string(default="")
instance = string(default="")
user_name = string(default="")
ignored_clients = list(default=list())
[general]
relative_times = boolean(default=True)
max_posts_per_call = integer(default=40)
reverse_timelines = boolean(default=False)
persist_size = integer(default=0)
load_cache_in_memory=boolean(default=True)
show_screen_names = boolean(default=False)
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications'))
boost_mode = string(default="ask")
[sound]
volume = float(default=1.0)
input_device = string(default="Default")
output_device = string(default="Default")
session_mute = boolean(default=False)
current_soundpack = string(default="FreakyBlue")
indicate_audio = boolean(default=True)
indicate_img = boolean(default=True)
[other_buffers]
timelines = list(default=list())
searches = list(default=list())
lists = list(default=list())
followers_timelines = list(default=list())
following_timelines = list(default=list())
trending_topic_buffers = list(default=list())
muted_buffers = list(default=list())
autoread_buffers = list(default=list(mentions, direct_messages, events))
[mysc]
spelling_language = string(default="")
save_followers_in_autocompletion_db = boolean(default=False)
save_friends_in_autocompletion_db = boolean(default=False)
ocr_language = string(default="")
[reporting]
braille_reporting = boolean(default=True)
speech_reporting = boolean(default=True)
[templates]
post = string(default="$display_name, $safe_text $image_descriptions $date. $visibility. $source")
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
conversation = string(default="Conversation with $users. Last message: $last_post")
[filters]
[user-aliases]

View File

@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
testmodules = ["test.test_cache"]
suite = unittest.TestSuite()
for t in testmodules:
try:
# If the module defines a suite() function, call it to get the suite.
mod = __import__(t, globals(), locals(), ['suite'])
suitefn = getattr(mod, 'suite')
suite.addTest(suitefn())
except (ImportError, AttributeError):
# else, just load all the test cases from the module.
suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t))
unittest.TextTestRunner(verbosity=2).run(suite)

View File

@@ -7,4 +7,3 @@ Contents of this package:
manager: Handles multiple sessions, setting the configuration files and check if the session is valid. Part of the model.
session: Creates a twitter session for an user. The other part of the model.
"""
from __future__ import unicode_literals

View File

@@ -1,57 +0,0 @@
from __future__ import unicode_literals
from gi.repository import Gtk
import widgetUtils
class sessionManagerWindow(widgetUtils.baseDialog):
def __init__(self):
super(sessionManagerWindow, self).__init__("Session Manager", None, 0, (Gtk.STOCK_OK, widgetUtils.OK, Gtk.STOCK_CANCEL, widgetUtils.CANCEL))
self.list = widgetUtils.list("Session")
self.box.add(self.list.list)
btnBox = Gtk.Box(spacing=6)
self.new = Gtk.Button("New account")
self.remove = Gtk.Button("Remove account")
self.configuration = Gtk.Button("Configuration")
btnBox.add(self.new)
btnBox.add(self.remove)
btnBox.add(self.configuration)
self.box.add(btnBox)
self.show_all()
def fill_list(self, sessionsList):
for i in sessionsList:
self.list.insert_item(False, i)
if self.list.get_count() > 0:
self.list.select_item(0)
def new_account_dialog(self):
dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, "Authorization")
dialog.format_secondary_text("The request to authorize your Twitter account will be opened in your browser. You only need to do this once. Would you like to continue?")
answer = dialog.run()
dialog.destroy()
return answer
def add_new_session_to_list(self):
total = self.list.get_count()
name = "Authorized account %d" % (total+1)
self.list.insert_item(name)
if self.list.get_count() == 1:
self.list.select_item(0)
def show_unauthorised_error(self):
dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.CANCEL, "Invalid user token")
dialog.format_secondary_text("Your access token is invalid or the authorization has failed. Please try again.")
answer = dialog.run()
return answer
def remove_account_dialog(self):
dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, "Remove account")
dialog.format_secondary_text("Do you really want delete this account?")
answer = dialog.run()
return answer
def get_selected(self):
return self.list.get_selected()
def remove_session(self, sessionID):
self.list.remove_item(sessionID)

View File

@@ -1,42 +1,25 @@
# -*- coding: cp1252 -*-
#from config_utils import Configuration, ConfigurationResetException
from __future__ import unicode_literals
from builtins import object
import config
import paths
""" Lightweigth module that saves session position across global config and performs validation of config files. """
import os
import logging
import config
import paths
log = logging.getLogger("sessionmanager.manager")
from sessions import session_exceptions
manager = None
def setup():
""" Creates the singleton instance used within TWBlue to access this object. """
global manager
if not manager:
manager = sessionManager()
class sessionManager(object):
# def __init__(self):
# FILE = "sessions.conf"
# SPEC = "app-configuration.defaults"
# try:
# self.main = Configuration(paths.config_path(FILE), paths.app_path(SPEC))
# except ConfigurationResetException:
# pass
def get_current_session(self):
""" Returns the currently focused session, if valid. """
if self.is_valid(config.app["sessions"]["current_session"]):
return config.app["sessions"]["current_session"]
else:
return False
def add_session(self, id):
log.debug("Adding a new session: %s" % (id,))
path = os.path.join(paths.config_path(), id)
if not os.path.exists(path):
log.debug("Creating %s path" % (os.path.join(paths.config_path(), path),))
os.mkdir(path)
config.app["sessions"]["sessions"].append(id)
def set_current_session(self, sessionID):
config.app["sessions"]["current_session"] = sessionID

View File

@@ -1,42 +1,51 @@
# -*- coding: utf-8 -*-
import shutil
import widgetUtils
import platform
import output
if platform.system() == "Windows":
from . import wxUI as view
from controller import settings
elif platform.system() == "Linux":
from . import gtkUI as view
import paths
""" Module to perform session actions such as addition, removal or display of the global settings dialogue. """
import time
import os
import logging
import widgetUtils
import sessions
from sessions.twitter import session
from . import manager
import output
import paths
import config_utils
import config
from pubsub import pub
from tweepy.errors import TweepyException
from controller import settings
from sessions.twitter import session as TwitterSession
from sessions.mastodon import session as MastodonSession
from . import manager
from . import wxUI as view
log = logging.getLogger("sessionmanager.sessionManager")
class sessionManagerController(object):
def __init__(self, started=False):
def __init__(self, started: bool = False):
""" Class constructor.
Creates the SessionManager class controller, responsible for the accounts within TWBlue. From this dialog, users can add/Remove accounts, or load the global settings dialog.
:param started: Indicates whether this object is being created during application startup (when no other controller has been instantiated) or not. It is important for us to know this, as we won't show the button to open global settings dialog if the application has been started. Users must choose the corresponding option in the menu bar.
:type started: bool
"""
super(sessionManagerController, self).__init__()
log.debug("Setting up the session manager.")
self.started = started
# Initialize the manager, responsible for storing session objects.
manager.setup()
self.view = view.sessionManagerWindow()
widgetUtils.connect_event(self.view.new, widgetUtils.BUTTON_PRESSED, self.manage_new_account)
widgetUtils.connect_event(self.view.remove, widgetUtils.BUTTON_PRESSED, self.remove)
pub.subscribe(self.manage_new_account, "sessionmanager.new_account")
pub.subscribe(self.remove_account, "sessionmanager.remove_account")
if self.started == False:
widgetUtils.connect_event(self.view.configuration, widgetUtils.BUTTON_PRESSED, self.configuration)
pub.subscribe(self.configuration, "sessionmanager.configuration")
else:
self.view.hide_configuration()
# Store a temporary copy of new and removed sessions, so we will perform actions on them during call to on_ok.
self.new_sessions = {}
self.removed_sessions = []
def fill_list(self):
""" Fills the session list with all valid sessions that could be found in config path. """
sessionsList = []
reserved_dirs = ["dicts"]
log.debug("Filling the sessions list.")
@@ -55,10 +64,16 @@ class sessionManagerController(object):
output.speak("An exception was raised while attempting to clean malformed session data. See the error log for details. If this message persists, contact the developers.",True)
os.exception("Exception thrown while removing malformed session")
continue
name = config_test["twitter"]["user_name"]
if config_test["twitter"]["user_key"] != "" and config_test["twitter"]["user_secret"] != "":
sessionsList.append(name)
self.sessions.append(i)
if config_test.get("twitter") != None:
name = _("{account_name} (Twitter)").format(account_name=config_test["twitter"]["user_name"])
if config_test["twitter"]["user_key"] != "" and config_test["twitter"]["user_secret"] != "":
sessionsList.append(name)
self.sessions.append(dict(type="twitter", id=i))
elif config_test.get("mastodon") != None:
name = _("{account_name} (Mastodon)").format(account_name=config_test["mastodon"]["user_name"])
if config_test["mastodon"]["instance"] != "" and config_test["mastodon"]["access_token"] != "":
sessionsList.append(name)
self.sessions.append(dict(type="mastodon", id=i))
else:
try:
log.debug("Deleting session %s" % (i,))
@@ -69,6 +84,7 @@ class sessionManagerController(object):
self.view.fill_list(sessionsList)
def show(self):
""" Displays the session manager dialog. """
if self.view.get_response() == widgetUtils.OK:
self.do_ok()
# else:
@@ -77,49 +93,49 @@ class sessionManagerController(object):
def do_ok(self):
log.debug("Starting sessions...")
for i in self.sessions:
if (i in sessions.sessions) == True: continue
s = session.Session(i)
# Skip already created sessions. Useful when session manager controller is not created during startup.
if sessions.sessions.get(i.get("id")) != None:
continue
# Create the session object based in session type.
if i.get("type") == "twitter":
s = TwitterSession.Session(i.get("id"))
elif i.get("type") == "mastodon":
s = MastodonSession.Session(i.get("id"))
s.get_configuration()
if i not in config.app["sessions"]["ignored_sessions"]:
if i.get("id") not in config.app["sessions"]["ignored_sessions"]:
try:
s.login()
except TweepyException:
self.show_auth_error(s.settings["twitter"]["user_name"])
continue
sessions.sessions[i] = s
self.new_sessions[i] = s
sessions.sessions[i.get("id")] = s
self.new_sessions[i.get("id")] = s
# self.view.destroy()
def show_auth_error(self, user_name):
error = view.auth_error(user_name)
def manage_new_account(self, *args, **kwargs):
if self.view.new_account_dialog() == widgetUtils.YES:
location = (str(time.time())[-6:])
log.debug("Creating session in the %s path" % (location,))
s = session.Session(location)
manager.manager.add_session(location)
s.get_configuration()
# try:
s.authorise()
self.sessions.append(location)
def manage_new_account(self, type):
# Generic settings for all account types.
location = (str(time.time())[-6:])
log.debug("Creating %s session in the %s path" % (type, location))
if type == "twitter":
s = TwitterSession.Session(location)
elif type == "mastodon":
s = MastodonSession.Session(location)
result = s.authorise()
if result == True:
self.sessions.append(dict(id=location, type=type))
self.view.add_new_session_to_list()
s.settings.write()
# except:
# log.exception("Error authorising the session")
# self.view.show_unauthorised_error()
# return
def remove(self, *args, **kwargs):
if self.view.remove_account_dialog() == widgetUtils.YES:
selected_account = self.sessions[self.view.get_selected()]
self.view.remove_session(self.view.get_selected())
self.removed_sessions.append(selected_account)
self.sessions.remove(selected_account)
shutil.rmtree(path=os.path.join(paths.config_path(), selected_account), ignore_errors=True)
def remove_account(self, index):
selected_account = self.sessions[index]
self.view.remove_session(index)
self.removed_sessions.append(selected_account.get("id"))
self.sessions.remove(selected_account)
shutil.rmtree(path=os.path.join(paths.config_path(), selected_account.get("id")), ignore_errors=True)
def configuration(self, *args, **kwargs):
def configuration(self):
""" Opens the global settings dialogue."""
d = settings.globalSettingsController()
if d.response == widgetUtils.OK:

View File

@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
""" Base GUI (Wx) class for the Session manager module."""
import wx
from pubsub import pub
from multiplatform_widgets import widgets
import application
class sessionManagerWindow(wx.Dialog):
""" Dialog that displays all session managing capabilities to users. """
def __init__(self):
super(sessionManagerWindow, self).__init__(parent=None, title=_(u"Session manager"), size=wx.DefaultSize)
panel = wx.Panel(self)
@@ -16,8 +17,11 @@ class sessionManagerWindow(wx.Dialog):
listSizer.Add(self.list.list, 0, wx.ALL, 5)
sizer.Add(listSizer, 0, wx.ALL, 5)
self.new = wx.Button(panel, -1, _(u"New account"), size=wx.DefaultSize)
self.new.Bind(wx.EVT_BUTTON, self.on_new_account)
self.remove = wx.Button(panel, -1, _(u"Remove account"))
self.remove.Bind(wx.EVT_BUTTON, self.on_remove)
self.configuration = wx.Button(panel, -1, _(u"Global Settings"))
self.configuration.Bind(wx.EVT_BUTTON, self.on_configuration)
ok = wx.Button(panel, wx.ID_OK, size=wx.DefaultSize)
ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL, size=wx.DefaultSize)
@@ -42,11 +46,29 @@ class sessionManagerWindow(wx.Dialog):
if self.list.get_count() == 0:
wx.MessageDialog(None, _(u"You need to configure an account."), _(u"Account Error"), wx.ICON_ERROR).ShowModal()
return
self.controller.do_ok()
self.EndModal(wx.ID_OK)
def new_account_dialog(self):
return wx.MessageDialog(self, _(u"The request to authorize your Twitter account will be opened in your browser. You only need to do this once. Would you like to continue?"), _(u"Authorization"), wx.YES_NO).ShowModal()
def on_new_account(self, *args, **kwargs):
menu = wx.Menu()
twitter = menu.Append(wx.ID_ANY, _("Twitter"))
mastodon = menu.Append(wx.ID_ANY, _("Mastodon"))
menu.Bind(wx.EVT_MENU, self.on_new_twitter_account, twitter)
menu.Bind(wx.EVT_MENU, self.on_new_mastodon_account, mastodon)
self.PopupMenu(menu, self.new.GetPosition())
def on_new_mastodon_account(self, *args, **kwargs):
dlg = wx.MessageDialog(self, _("You will be prompted for your Mastodon data (instance URL, email address and password) so we can authorise TWBlue in your instance. Would you like to authorise your account now?"), _(u"Authorization"), wx.YES_NO)
response = dlg.ShowModal()
dlg.Destroy()
if response == wx.ID_YES:
pub.sendMessage("sessionmanager.new_account", type="mastodon")
def on_new_twitter_account(self, *args, **kwargs):
dlg = wx.MessageDialog(self, _(u"The request to authorize your Twitter account will be opened in your browser. You only need to do this once. Would you like to continue?"), _(u"Authorization"), wx.YES_NO)
response = dlg.ShowModal()
dlg.Destroy()
if response == wx.ID_YES:
pub.sendMessage("sessionmanager.new_account", type="twitter")
def add_new_session_to_list(self):
total = self.list.get_count()
@@ -61,8 +83,16 @@ class sessionManagerWindow(wx.Dialog):
def get_response(self):
return self.ShowModal()
def remove_account_dialog(self):
return wx.MessageDialog(self, _(u"Do you really want to delete this account?"), _(u"Remove account"), wx.YES_NO).ShowModal()
def on_remove(self, *args, **kwargs):
dlg = wx.MessageDialog(self, _(u"Do you really want to delete this account?"), _(u"Remove account"), wx.YES_NO)
response = dlg.ShowModal()
dlg.Destroy()
if response == wx.ID_YES:
selected = self.list.get_selected()
pub.sendMessage("sessionmanager.remove_account", index=selected)
def on_configuration(self, *args, **kwargs):
pub.sendMessage("sessionmanager.configuration")
def get_selected(self):
return self.list.get_selected()

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
""" this package contains code related to Sessions.
In TWBlue, a session module defines everything a social network needs to be used in the program."""
from __future__ import unicode_literals
# let's define a global object for storing sessions across the program.
sessions = {}

View File

@@ -6,10 +6,12 @@ import output
import time
import sound
import logging
import config
import config_utils
import sqlitedict
import application
from . import session_exceptions as Exceptions
log = logging.getLogger("sessionmanager.session")
class baseSession(object):
@@ -52,6 +54,13 @@ class baseSession(object):
def is_logged(self):
return self.logged
def create_session_folder(self):
path = os.path.join(paths.config_path(), self.session_id)
if not os.path.exists(path):
log.debug("Creating %s path" % (os.path.join(paths.config_path(), path),))
os.mkdir(path)
config.app["sessions"]["sessions"].append(id)
def get_configuration(self):
""" Get settings for a session."""
file_ = "%s/session.conf" % (self.session_id,)
@@ -60,6 +69,9 @@ class baseSession(object):
self.init_sound()
self.load_persistent_data()
def get_name(self):
pass
def init_sound(self):
try: self.sound = sound.soundSystem(self.settings["sound"])
except: pass

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import arrow
import languageHandler
from . import utils, templates
def compose_post(post, db, relative_times, show_screen_names):
if show_screen_names == False:
user = post.account.get("display_name")
if user == "":
user = post.account.get("username")
else:
user = post.account.get("acct")
original_date = arrow.get(post.created_at)
if relative_times:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
if post.reblog != None:
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog))
else:
text = templates.process_text(post)
source = post.get("application", "")
# "" means remote user, None for legacy apps so we should cover both sides.
if source != None and source != "":
source = source.get("name", "")
else:
source = ""
return [user+", ", text, ts+", ", source]
def compose_user(user, db, relative_times=True, show_screen_names=False):
original_date = arrow.get(user.created_at)
if relative_times:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
name = user.display_name
if name == "":
name = user.get("username")
return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (name, user.acct, user.followers_count, user.following_count, user.statuses_count, ts)]
def compose_conversation(conversation, db, relative_times, show_screen_names):
users = []
for account in conversation.accounts:
if account.display_name != "":
users.append(account.display_name)
else:
users.append(account.username)
users = ", ".join(users)
last_post = compose_post(conversation.last_status, db, relative_times, show_screen_names)
text = _("Last message from {}: {}").format(last_post[0], last_post[1])
return [users, text, last_post[-2], last_post[-1]]

View File

@@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
import os
import paths
import time
import logging
import webbrowser
import wx
import mastodon
import config
import config_utils
import output
import application
from mastodon import MastodonError, MastodonNotFoundError, MastodonUnauthorizedError
from pubsub import pub
from mysc.thread_utils import call_threaded
from sessions import base
from sessions.mastodon import utils, streaming
from .wxUI import authorisationDialog
log = logging.getLogger("sessions.mastodonSession")
MASTODON_VERSION = "4.0.1"
class Session(base.baseSession):
def __init__(self, *args, **kwargs):
super(Session, self).__init__(*args, **kwargs)
self.config_spec = "mastodon.defaults"
self.supported_languages = []
self.type = "mastodon"
self.db["pagination_info"] = dict()
self.char_limit = 500
pub.subscribe(self.on_status, "mastodon.status_received")
pub.subscribe(self.on_status_updated, "mastodon.status_updated")
pub.subscribe(self.on_notification, "mastodon.notification_received")
def login(self, verify_credentials=True):
if self.settings["mastodon"]["access_token"] != None and self.settings["mastodon"]["instance"] != None:
try:
log.debug("Logging in to Mastodon instance {}...".format(self.settings["mastodon"]["instance"]))
self.api = mastodon.Mastodon(access_token=self.settings["mastodon"]["access_token"], api_base_url=self.settings["mastodon"]["instance"], mastodon_version=MASTODON_VERSION)
if verify_credentials == True:
credentials = self.api.account_verify_credentials()
self.db["user_name"] = credentials["username"]
self.db["user_id"] = credentials["id"]
self.settings["mastodon"]["user_name"] = credentials["username"]
self.logged = True
log.debug("Logged.")
self.counter = 0
except IOError:
log.error("The login attempt failed.")
self.logged = False
else:
self.logged = False
raise Exceptions.RequireCredentialsSessionError
def authorise(self):
if self.logged == True:
raise Exceptions.AlreadyAuthorisedError("The authorisation process is not needed at this time.")
authorisation_dialog = wx.TextEntryDialog(None, _("Please enter your instance URL."), _("Mastodon instance"))
answer = authorisation_dialog.ShowModal()
instance = authorisation_dialog.GetValue()
authorisation_dialog.Destroy()
if answer != wx.ID_OK:
return
try:
client_id, client_secret = mastodon.Mastodon.create_app("TWBlue", api_base_url=authorisation_dialog.GetValue(), website="https://twblue.es")
temporary_api = mastodon.Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=instance, mastodon_version=MASTODON_VERSION)
auth_url = temporary_api.auth_request_url()
except MastodonError:
dlg = wx.MessageDialog(None, _("We could not connect to your mastodon instance. Please verify that the domain exists and the instance is accessible via a web browser."), _("Instance error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
return
webbrowser.open_new_tab(auth_url)
verification_dialog = wx.TextEntryDialog(None, _("Enter the verification code"), _("PIN code authorization"))
answer = verification_dialog.ShowModal()
code = verification_dialog.GetValue()
verification_dialog.Destroy()
if answer != wx.ID_OK:
return
try:
access_token = temporary_api.log_in(code=verification_dialog.GetValue())
except MastodonError:
dlg = wx.MessageDialog(None, _("We could not authorice your mastodon account to be used in TWBlue. This might be caused due to an incorrect verification code. Please try to add the session again."), _("Authorization error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
return
self.create_session_folder()
self.get_configuration()
self.settings["mastodon"]["access_token"] = access_token
self.settings["mastodon"]["instance"] = instance
self.settings.write()
return True
def get_user_info(self):
""" Retrieves some information required by TWBlue for setup."""
# retrieve the current user's UTC offset so we can calculate dates properly.
offset = time.timezone if (time.localtime().tm_isdst == 0) else time.altzone
offset = offset / 60 / 60 * -1
self.db["utc_offset"] = offset
if len(self.supported_languages) == 0:
self.supported_languages = self.api.instance().languages
self.get_lists()
self.get_muted_users()
# determine instance custom characters limit.
instance = self.api.instance()
if hasattr(instance, "configuration") and hasattr(instance.configuration, "statuses") and hasattr(instance.configuration.statuses, "max_characters"):
self.char_limit = instance.configuration.statuses.max_characters
self.settings.write()
def get_lists(self):
""" Gets the lists that the user is subscribed to and stores them in the database. Returns None."""
self.db["lists"] = self.api.lists()
def get_muted_users(self):
### ToDo: Use a function to retrieve all muted users.
self.db["muted_users"] = self.api.mutes()
def get_user_alias(self, user):
aliases = self.settings.get("user-aliases")
if aliases == None:
log.error("Aliases are not defined for this config spec.")
return user.name
user_alias = aliases.get(user.id_str)
if user_alias != None:
return user_alias
return user.name
def order_buffer(self, name, data, ignore_older=False):
num = 0
last_id = None
if self.db.get(name) == None:
self.db[name] = []
objects = self.db[name]
if ignore_older and len(self.db[name]) > 0:
if self.settings["general"]["reverse_timelines"] == False:
last_id = self.db[name][0].id
else:
last_id = self.db[name][-1].id
for i in data:
if ignore_older and last_id != None:
if i.id < last_id:
log.error("Ignoring an older tweet... Last id: {0}, tweet id: {1}".format(last_id, i.id))
continue
if utils.find_item(i, self.db[name]) == None:
if self.settings["general"]["reverse_timelines"] == False: objects.append(i)
else: objects.insert(0, i)
num = num+1
self.db[name] = objects
return num
def update_item(self, name, item):
if name not in self.db:
return False
items = self.db[name]
if type(items) != list:
return False
# determine item position in buffer.
item_position = next((x for x in range(len(items)) if items[x].id == item.id), None)
if item_position != None:
self.db[name][item_position] = item
return item_position
return False
def api_call(self, call_name, action="", _sound=None, report_success=False, report_failure=True, preexec_message="", *args, **kwargs):
finished = False
tries = 0
if preexec_message:
output.speak(preexec_message, True)
while finished==False and tries < 25:
try:
val = getattr(self.api, call_name)(*args, **kwargs)
finished = True
except MastodonError as e:
output.speak(str(e))
val = None
if type(e) != MastodonNotFoundError and type(e) != MastodonUnauthorizedError :
tries = tries+1
time.sleep(5)
elif report_failure:
output.speak(_("%s failed. Reason: %s") % (action, str(e)))
finished = True
# except:
# tries = tries + 1
# time.sleep(5)
if report_success:
output.speak(_("%s succeeded.") % action)
if _sound != None: self.sound.play(_sound)
return val
def send_post(self, reply_to=None, users=None, visibility=None, posts=[]):
""" Convenience function to send a thread. """
in_reply_to_id = reply_to
for obj in posts:
text = obj.get("text")
if len(obj["attachments"]) == 0:
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, visibility=visibility, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"])
if item != None:
in_reply_to_id = item["id"]
else:
media_ids = []
poll = None
if len(obj["attachments"]) == 1 and obj["attachments"][0]["type"] == "poll":
poll = self.api.make_poll(options=obj["attachments"][0]["options"], expires_in=obj["attachments"][0]["expires_in"], multiple=obj["attachments"][0]["multiple"], hide_totals=obj["attachments"][0]["hide_totals"])
else:
for i in obj["attachments"]:
img = self.api_call("media_post", media_file=i["file"], description=i["description"])
media_ids.append(img.id)
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, media_ids=media_ids, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"])
if item != None:
in_reply_to_id = item["id"]
def get_name(self):
instance = self.settings["mastodon"]["instance"]
instance = instance.replace("https://", "")
user = self.settings["mastodon"]["user_name"]
return "Mastodon: {}@{}".format(user, instance)
def start_streaming(self):
if config.app["app-settings"]["no_streaming"]:
return
listener = streaming.StreamListener(session_name=self.get_name(), user_id=self.db["user_id"])
self.user_stream = self.api.stream_user(listener, run_async=True)
self.direct_stream = self.api.stream_direct(listener, run_async=True)
def stop_streaming(self):
if config.app["app-settings"]["no_streaming"]:
return
# if hasattr(self, "user_stream"):
# self.user_stream.close()
# log.debug("Stream stopped for accounr {}".format(self.db["user_name"]))
# if hasattr(self, "direct_stream"):
# self.direct_stream.close()
# log.debug("Stream stopped for accounr {}".format(self.db["user_name"]))
def check_streams(self):
if config.app["app-settings"]["no_streaming"]:
return
if not hasattr(self, "user_stream"):
return
if self.user_stream.is_alive() == False or self.user_stream.is_receiving() == False or self.direct_stream.is_alive() == False or self.direct_stream.is_receiving() == False:
self.start_streaming()
def check_buffers(self, status):
buffers = []
buffers.append("home_timeline")
if status.account.id == self.db["user_id"]:
buffers.append("sent")
return buffers
def on_status(self, status, session_name):
# Discard processing the status if the streaming sends a tweet for another account.
if self.get_name() != session_name:
return
buffers = self.check_buffers(status)
for b in buffers[::]:
num = self.order_buffer(b, [status])
if num == 0:
buffers.remove(b)
pub.sendMessage("mastodon.new_item", session_name=self.get_name(), item=status, _buffers=buffers)
def on_status_updated(self, status, session_name):
# Discard processing the status if the streaming sends a tweet for another account.
if self.get_name() != session_name:
return
buffers = {}
for b in list(self.db.keys()):
updated = self.update_item(b, status)
if updated != False:
buffers[b] = updated
pub.sendMessage("mastodon.updated_item", session_name=self.get_name(), item=status, _buffers=buffers)
def on_notification(self, notification, session_name):
# Discard processing the notification if the streaming sends a tweet for another account.
if self.get_name() != session_name:
return
buffers = []
obj = None
if notification.type == "mention":
buffers = ["mentions"]
obj = notification.status
elif notification.type == "follow":
buffers = ["followers"]
obj = notification.account
for b in buffers[::]:
num = self.order_buffer(b, [obj])
if num == 0:
buffers.remove(b)
pub.sendMessage("mastodon.new_item", session_name=self.get_name(), item=obj, _buffers=buffers)

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
import mastodon
from pubsub import pub
class StreamListener(mastodon.StreamListener):
def __init__(self, session_name, user_id):
self.session_name = session_name
self.user_id = user_id
super(StreamListener, self).__init__()
def on_update(self, status):
pub.sendMessage("mastodon.status_received", status=status, session_name=self.session_name)
def on_status_update(self, status):
pub.sendMessage("mastodon.status_updated", status=status, session_name=self.session_name)
def on_conversation(self, conversation):
pub.sendMessage("mastodon.conversation_received", conversation=conversation, session_name=self.session_name)
def on_notification(self, notification):
pub.sendMessage("mastodon.notification_received", notification=notification, session_name=self.session_name)
def on_unknown_event(self, event, payload):
log.error("Unknown event: {} with payload as {}".format(event, payload))

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
import re
import arrow
import languageHandler
from string import Template
from . import utils, compose
# Define variables that would be available for all template objects.
# This will be used for the edit template dialog.
# Available variables for post objects.
# safe_text will be the content warning in case a post contains one, text will always be the full text, no matter if has a content warning or not.
post_variables = ["date", "display_name", "screen_name", "source", "lang", "safe_text", "text", "image_descriptions", "visibility"]
person_variables = ["display_name", "screen_name", "description", "followers", "following", "favorites", "posts", "created_at"]
conversation_variables = ["users", "last_post"]
# Default, translatable templates.
post_default_template = _("$display_name, $text $image_descriptions $date. $source")
dm_sent_default_template = _("Dm to $recipient_display_name, $text $date")
person_default_template = _("$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
def process_date(field, relative_times=True, offset_hours=0):
original_date = arrow.get(field)
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(hours=offset_hours).format(_("dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
return ts
def process_text(post, safe=True):
# text = utils.clean_mentions(utils.StripChars(text))
if safe == True and post.sensitive == True and post.spoiler_text != "":
return _("Content warning: {}").format(post.spoiler_text)
return utils.html_filter(post.content)
def process_image_descriptions(media_attachments):
""" Attempt to extract information for image descriptions. """
image_descriptions = []
for media in media_attachments:
if media.get("description") != None and media.get("description") != "":
image_descriptions.append(media.get("description"))
idescriptions = ""
for image in image_descriptions:
idescriptions = idescriptions + _("Image description: {}").format(image) + "\n"
return idescriptions
def remove_unneeded_variables(template, variables):
for variable in variables:
template = re.sub("\$"+variable, "", template)
return template
def render_post(post, template, relative_times=False, offset_hours=0):
""" Renders any given post according to the passed template.
Available data for posts will be stored in the following variables:
$date: Creation date.
$display_name: User profile name.
$screen_name: User screen name, this is the same name used to reference the user in Twitter.
$ source: Source client from where the current tweet was sent.
$lang: Two letter code for the automatically detected language for the tweet. This detection is performed by Twitter.
$safe_text: Safe text to display. If a content warning is applied in posts, display those instead of the whole post.
$text: Toot text. This always displays the full text, even if there is a content warning present.
$image_descriptions: Information regarding image descriptions added by twitter users.
$visibility: post's visibility: public, not listed, followers only or direct.
"""
global post_variables
available_data = dict()
created_at = process_date(post.created_at, relative_times, offset_hours)
available_data.update(date=created_at)
# user.
display_name = post.account.display_name
if display_name == "":
display_name = post.account.username
available_data.update(display_name=display_name, screen_name=post.account.acct)
# Source client from where tweet was originated.
source = ""
if hasattr(post, "application") and post.application != None:
available_data.update(source=post.application.get("name"))
if post.reblog != None:
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, process_text(post.reblog, safe=False), )
safe_text = _("Boosted from @{}: {}").format(post.reblog.account.acct, process_text(post.reblog), )
else:
text = process_text(post, safe=False)
safe_text = process_text(post)
visibility_settings = dict(public=_("Public"), unlisted=_("Not listed"), private=_("Followers only"), direct=_("Direct"))
visibility = visibility_settings.get(post.visibility)
available_data.update(lang=post.language, text=text, safe_text=safe_text, visibility=visibility)
# process image descriptions
image_descriptions = ""
if post.reblog != None:
image_descriptions = process_image_descriptions(post.reblog.media_attachments)
else:
image_descriptions = process_image_descriptions(post.media_attachments)
if image_descriptions != "":
available_data.update(image_descriptions=image_descriptions)
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, post_variables)
return result
def render_user(user, template, relative_times=True, offset_hours=0):
""" Renders persons by using the provided template.
Available data will be stored in the following variables:
$display_name: The name of the user, as theyve defined it. Not necessarily a persons name. Typically capped at 50 characters, but subject to change.
$screen_name: The screen name, handle, or alias that this user identifies themselves with.
$description: The user-defined UTF-8 string describing their account.
$followers: The number of followers this account currently has. This value might be inaccurate.
$following: The number of users this account is following (AKA their “followings”). This value might be inaccurate.
$posts: The number of Tweets (including retweets) issued by the user. This value might be inaccurate.
$created_at: The date and time that the user account was created on Twitter.
"""
global person_variables
display_name = user.display_name
if display_name == "":
display_name = user.username
available_data = dict(display_name=display_name, screen_name=user.acct, followers=user.followers_count, following=user.following_count, posts=user.statuses_count)
# Nullable values.
nullables = ["description"]
for nullable in nullables:
if hasattr(user, nullable) and getattr(user, nullable) != None:
available_data[nullable] = getattr(user, nullable)
created_at = process_date(user.created_at, relative_times=relative_times, offset_hours=offset_hours)
available_data.update(created_at=created_at)
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, person_variables)
return result
def render_conversation(conversation, template, post_template, relative_times=False, offset_hours=0):
users = []
for account in conversation.accounts:
if account.display_name != "":
users.append(account.display_name)
else:
users.append(account.username)
users = ", ".join(users)
last_post = render_post(conversation.last_status, post_template, relative_times=relative_times, offset_hours=offset_hours)
available_data = dict(users=users, last_post=last_post)
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, conversation_variables)
return result

View File

@@ -0,0 +1,60 @@
import re
from html.parser import HTMLParser
url_re = re.compile('<a\s*href=[\'|"](.*?)[\'"].*?>')
class HTMLFilter(HTMLParser):
text = ""
def handle_data(self, data):
self.text += data
def handle_starttag(self, tag, attrs):
if tag == "br":
self.text = self.text+"\n"
def html_filter(data):
f = HTMLFilter()
f.feed(data)
return f.text
def find_item(item, listItems):
for i in range(0, len(listItems)):
if listItems[i].id == item.id:
return i
if hasattr(item, "reblog") and item.reblog != None and item.reblog.id == listItems[i].id:
return i
return None
def is_audio_or_video(post):
if post.reblog != None:
return is_audio_or_video(post.reblog)
# Checks firstly for Mastodon native videos and audios.
for media in post.media_attachments:
if media["type"] == "video" or media["type"] == "audio":
return True
def is_image(post):
if post.reblog != None:
return is_image(post.reblog)
# Checks firstly for Mastodon native videos and audios.
for media in post.media_attachments:
if media["type"] == "gifv" or media["type"] == "image":
return True
def get_media_urls(post):
if hasattr(post, "reblog") and post.reblog != None:
return get_media_urls(post.reblog)
urls = []
for media in post.media_attachments:
if media.get("type") == "audio" or media.get("type") == "video":
urls.append(media.get("url"))
return urls
def find_urls(post, include_tags=False):
urls = url_re.findall(post.content)
if include_tags == False:
for tag in post.tags:
for url in urls[::]:
if url.lower().endswith("/tags/"+tag["name"]):
urls.remove(url)
return urls

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
import wx
class authorisationDialog(wx.Dialog):
def __init__(self):
super(authorisationDialog, self).__init__(parent=None, title=_(u"Authorising account..."))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
static1 = wx.StaticText(panel, wx.NewId(), _("URL of mastodon instance:"))
self.instance = wx.TextCtrl(panel, -1)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(static1, 0, wx.ALL, 5)
sizer1.Add(self.instance, 0, wx.ALL, 5)
sizer.Add(sizer1, 0, wx.ALL, 5)
static2 = wx.StaticText(panel, wx.NewId(), _("Email address:"))
self.email = wx.TextCtrl(panel, -1)
sizer2 = wx.BoxSizer(wx.HORIZONTAL)
sizer2.Add(static2, 0, wx.ALL, 5)
sizer2.Add(self.email, 0, wx.ALL, 5)
sizer.Add(sizer2, 0, wx.ALL, 5)
static3 = wx.StaticText(panel, wx.NewId(), _("Password:"))
self.password = wx.TextCtrl(panel, -1)
sizer3 = wx.BoxSizer(wx.HORIZONTAL)
sizer3.Add(static3, 0, wx.ALL, 5)
sizer3.Add(self.password, 0, wx.ALL, 5)
sizer.Add(sizer3, 0, wx.ALL, 5)
self.ok = wx.Button(panel, wx.ID_OK)
self.cancel = wx.Button(panel, wx.ID_CANCEL)
sizer4 = wx.BoxSizer(wx.HORIZONTAL)
sizer4.Add(self.ok, 0, wx.ALL, 5)
sizer4.Add(self.cancel, 0, wx.ALL, 5)
sizer.Add(sizer4, 0, wx.ALL, 5)
panel.SetSizer(sizer)
min = sizer.CalcMin()
self.SetClientSize(min)

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import platform
system = platform.system()
from . import utils
import re
import time
@@ -17,14 +15,11 @@ chars = "abcdefghijklmnopqrstuvwxyz"
def compose_tweet(tweet, db, relative_times, show_screen_names=False, session=None):
""" It receives a tweet and returns a list with the user, text for the tweet or message, date and the client where user is."""
if system == "Windows":
original_date = arrow.get(tweet.created_at, locale="en")
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
original_date = arrow.get(tweet.created_at, locale="en")
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = tweet.created_at
ts = original_date.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
if hasattr(tweet, "message"):
value = "message"
elif hasattr(tweet, "full_text"):
@@ -58,16 +53,13 @@ def compose_tweet(tweet, db, relative_times, show_screen_names=False, session=No
return [user+", ", text, ts+", ", source]
def compose_direct_message(item, db, relative_times, show_screen_names=False, session=None):
if system == "Windows":
# Let's remove the last 3 digits in the timestamp string.
# Twitter sends their "epoch" timestamp with 3 digits for milliseconds and arrow doesn't like it.
original_date = arrow.get(int(item.created_timestamp))
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
# Let's remove the last 3 digits in the timestamp string.
# Twitter sends their "epoch" timestamp with 3 digits for milliseconds and arrow doesn't like it.
original_date = arrow.get(int(item.created_timestamp))
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = item.created_timestamp
ts = original_date.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
text = StripChars(item.message_create["message_data"]["text"])
source = "DM"
sender = session.get_user(item.message_create["sender_id"])
@@ -125,23 +117,17 @@ def compose_quoted_tweet(quoted_tweet, original_tweet, show_screen_names=False,
return quoted_tweet
def compose_followers_list(tweet, db, relative_times=True, show_screen_names=False, session=None):
if system == "Windows":
original_date = arrow.get(tweet.created_at, locale="en")
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
original_date = arrow.get(tweet.created_at, locale="en")
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = tweet.created_at
ts = original_date.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
if hasattr(tweet, "status"):
if system == "Windows":
original_date2 = arrow.get(tweet.status.created_at, locale="en")
if relative_times:
ts2 = original_date2.humanize(locale=languageHandler.curLang[:2])
else:
ts2 = original_date2.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
original_date2 = arrow.get(tweet.status.created_at, locale="en")
if relative_times:
ts2 = original_date2.humanize(locale=languageHandler.curLang[:2])
else:
ts2 = _("Unavailable")
ts2 = original_date2.shift(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
else:
ts2 = _("Unavailable")
return [_(u"%s (@%s). %s followers, %s friends, %s tweets. Last tweeted %s. Joined Twitter %s") % (tweet.name, tweet.screen_name, tweet.followers_count, tweet.friends_count, tweet.statuses_count, ts2, ts)]

View File

@@ -1,3 +1,2 @@
# -*- coding: utf-8 -*-
""" this package holds different modules to extract information regarding long tweets. A long tweet contains more than one tweet (such a quoted tweet), or is made via services like twishort."""
from __future__ import unicode_literals

View File

@@ -5,13 +5,13 @@ import time
import logging
import webbrowser
import wx
import tweepy
import demoji
import config
import output
import application
import appkeys
from pubsub import pub
import tweepy
from tweepy.errors import TweepyException, Forbidden, NotFound
from tweepy.models import User as UserModel
from mysc.thread_utils import call_threaded
@@ -106,10 +106,8 @@ class Session(base.baseSession):
else: objects.insert(0, i)
incoming = incoming+1
self.db["direct_messages"] = objects
self.db["sent_direct_messages"] = sent_objects
pub.sendMessage("sent-dms-updated", total=sent, account=self.db["user_name"])
pub.sendMessage("twitter.sent_dms_updated", total=sent, session_name=self.get_name())
return incoming
def __init__(self, *args, **kwargs):
@@ -127,8 +125,8 @@ class Session(base.baseSession):
# If we wouldn't implement this approach, TWBlue would save permanently the "deleted user" object.
self.deleted_users = {}
self.type = "twitter"
pub.subscribe(self.handle_new_status, "newStatus")
pub.subscribe(self.handle_connected, "streamConnected")
pub.subscribe(self.handle_new_status, "twitter.new_status")
pub.subscribe(self.handle_connected, "twitter.stream_connected")
# @_require_configuration
def login(self, verify_credentials=True):
@@ -137,12 +135,14 @@ class Session(base.baseSession):
if self.settings["twitter"]["user_key"] != None and self.settings["twitter"]["user_secret"] != None:
try:
log.debug("Logging in to twitter...")
self.auth = tweepy.OAuth1UserHandler(appkeys.twitter_api_key, appkeys.twitter_api_secret)
self.auth.set_access_token(self.settings["twitter"]["user_key"], self.settings["twitter"]["user_secret"])
self.auth = tweepy.OAuth1UserHandler(consumer_key=appkeys.twitter_api_key, consumer_secret=appkeys.twitter_api_secret, access_token=self.settings["twitter"]["user_key"], access_token_secret=self.settings["twitter"]["user_secret"])
self.twitter = tweepy.API(self.auth)
self.twitter_v2 = tweepy.Client(consumer_key=appkeys.twitter_api_key, consumer_secret=appkeys.twitter_api_secret, access_token=self.settings["twitter"]["user_key"], access_token_secret=self.settings["twitter"]["user_secret"])
if verify_credentials == True:
self.credentials = self.twitter.verify_credentials()
self.settings["twitter"]["user_name"] = self.credentials.screen_name
self.db["user_name"] = self.credentials.screen_name
self.db["user_id"] = self.credentials.id_str
self.logged = True
log.debug("Logged.")
self.counter = 0
@@ -153,37 +153,32 @@ class Session(base.baseSession):
self.logged = False
raise Exceptions.RequireCredentialsSessionError
# @_require_configuration
def authorise(self):
""" Authorises a Twitter account. This function needs to be called for each new session, after self.get_configuration() and before self.login()"""
if self.logged == True:
raise Exceptions.AlreadyAuthorisedError("The authorisation process is not needed at this time.")
else:
self.auth = tweepy.OAuth1UserHandler(appkeys.twitter_api_key, appkeys.twitter_api_secret)
redirect_url = self.auth.get_authorization_url()
webbrowser.open_new_tab(redirect_url)
self.authorisation_dialog = authorisationDialog()
self.authorisation_dialog.cancel.Bind(wx.EVT_BUTTON, self.authorisation_cancelled)
self.authorisation_dialog.ok.Bind(wx.EVT_BUTTON, self.authorisation_accepted)
self.authorisation_dialog.ShowModal()
def verify_authorisation(self, pincode):
self.auth.get_access_token(pincode)
self.settings["twitter"]["user_key"] = self.auth.access_token
self.settings["twitter"]["user_secret"] = self.auth.access_token_secret
auth = tweepy.OAuth1UserHandler(appkeys.twitter_api_key, appkeys.twitter_api_secret)
redirect_url = auth.get_authorization_url()
webbrowser.open_new_tab(redirect_url)
verification_dialog = wx.TextEntryDialog(None, _("Enter your PIN code here"), _("Authorising account..."))
answer = verification_dialog.ShowModal()
code = verification_dialog.GetValue()
verification_dialog.Destroy()
if answer != wx.ID_OK:
return
try:
auth.get_access_token(code)
except TweepyException:
dlg = wx.MessageDialog(None, _("We could not authorice your Twitter account to be used in TWBlue. This might be caused due to an incorrect verification code. Please try to add the session again."), _("Authorization error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
return False
self.create_session_folder()
self.get_configuration()
self.settings["twitter"]["user_key"] = auth.access_token
self.settings["twitter"]["user_secret"] = auth.access_token_secret
self.settings.write()
del self.auth
def authorisation_cancelled(self, *args, **kwargs):
""" Destroy the authorization dialog. """
self.authorisation_dialog.Destroy()
del self.authorisation_dialog
def authorisation_accepted(self, *args, **kwargs):
""" Gets the PIN code entered by user and validate it through Twitter."""
pincode = self.authorisation_dialog.text.GetValue()
self.verify_authorisation(pincode)
self.authorisation_dialog.Destroy()
return True
def api_call(self, call_name, action="", _sound=None, report_success=False, report_failure=True, preexec_message="", *args, **kwargs):
""" Make a call to the Twitter API. If there is a connectionError or another exception not related to Twitter, It will call the method again at least 25 times, waiting a while between calls. Useful for post methods.
@@ -279,15 +274,9 @@ class Session(base.baseSession):
# @_require_login
def get_user_info(self):
""" Retrieves some information required by TWBlue for setup."""
f = self.twitter.get_settings()
sn = f["screen_name"]
self.settings["twitter"]["user_name"] = sn
self.db["user_name"] = sn
self.db["user_id"] = self.twitter.get_user(screen_name=sn).id
try:
self.db["utc_offset"] = f["time_zone"]["utc_offset"]
except KeyError:
self.db["utc_offset"] = -time.timezone
offset = time.timezone if (time.localtime().tm_isdst == 0) else time.altzone
offset = offset*-1
self.db["utc_offset"] = offset
# Get twitter's supported languages and save them in a global variable
#so we won't call to this method once per session.
if len(application.supported_languages) == 0:
@@ -556,7 +545,7 @@ class Session(base.baseSession):
def start_streaming(self):
if config.app["app-settings"]["no_streaming"]:
return
self.stream = streaming.Stream(twitter_api=self.twitter, user=self.db["user_name"], user_id=self.db["user_id"], muted_users=self.db["muted_users"], consumer_key=appkeys.twitter_api_key, consumer_secret=appkeys.twitter_api_secret, access_token=self.settings["twitter"]["user_key"], access_token_secret=self.settings["twitter"]["user_secret"], chunk_size=1025)
self.stream = streaming.Stream(twitter_api=self.twitter, session_name=self.get_name(), user_id=self.db["user_id"], muted_users=self.db["muted_users"], consumer_key=appkeys.twitter_api_key, consumer_secret=appkeys.twitter_api_secret, access_token=self.settings["twitter"]["user_key"], access_token_secret=self.settings["twitter"]["user_secret"], chunk_size=1025)
self.stream_thread = call_threaded(self.stream.filter, follow=self.stream.users, stall_warnings=True)
def stop_streaming(self):
@@ -566,12 +555,12 @@ class Session(base.baseSession):
self.stream.running = False
log.debug("Stream stopped for accounr {}".format(self.db["user_name"]))
def handle_new_status(self, status, user):
def handle_new_status(self, status, session_name):
""" Handles a new status present in the Streaming API. """
if self.logged == False:
return
# Discard processing the status if the streaming sends a tweet for another account.
if self.db["user_name"] != user:
if self.get_name() != session_name:
return
# the Streaming API sends non-extended tweets with an optional parameter "extended_tweets" which contains full_text and other data.
# so we have to make sure we check it before processing the normal status.
@@ -608,7 +597,7 @@ class Session(base.baseSession):
status = self.check_quoted_status(status)
status = self.check_long_tweet(status)
# Send it to the main controller object.
pub.sendMessage("newTweet", data=status, user=self.db["user_name"], _buffers=buffers_to_send)
pub.sendMessage("twitter.new_tweet", data=status, session_name=self.get_name(), _buffers=buffers_to_send)
def check_streams(self):
if config.app["app-settings"]["no_streaming"]:
@@ -619,11 +608,11 @@ class Session(base.baseSession):
if self.stream.running == False:
self.start_streaming()
def handle_connected(self, user):
def handle_connected(self, session_name):
if self.logged == False:
return
if user != self.db["user_name"]:
log.debug("Connected streaming endpoint on account {}".format(user))
if session_name != self.get_name():
log.debug("Connected streaming endpoint on session {}".format(session_name))
def send_tweet(self, *tweets):
""" Convenience function to send a thread. """
@@ -674,4 +663,10 @@ class Session(base.baseSession):
else:
sent_dms.insert(0, item)
self.db["sent_direct_messages"] = sent_dms
pub.sendMessage("sent-dm", data=item, user=self.db["user_name"])
pub.sendMessage("twitter.sent_dm", data=item, session_name=self.get_name())
def get_name(self):
if self.logged:
return "Twitter: {}".format(self.db["user_name"])
else:
return "Twitter: {}".format(self.settings["twitter"]["user_name"])

View File

@@ -14,13 +14,13 @@ log = logging.getLogger("sessions.twitter.streaming")
class Stream(tweepy.Stream):
def __init__(self, twitter_api, user, user_id, muted_users=[], *args, **kwargs):
def __init__(self, twitter_api, session_name, user_id, muted_users=[], *args, **kwargs):
super(Stream, self).__init__(*args, **kwargs)
log.debug("Starting streaming listener for account {}".format(user))
log.debug("Starting streaming listener for session {}".format(session_name))
self.started = False
self.users = []
self.api = twitter_api
self.user = user
self.session_name = session_name
self.user_id = user_id
friends = self.api.get_friend_ids()
log.debug("Retrieved {} friends to add to the streaming listener.".format(len(friends)))
@@ -33,15 +33,15 @@ class Stream(tweepy.Stream):
log.debug("Streaming listener started with {} users to follow.".format(len(self.users)))
def on_connect(self):
pub.sendMessage("streamConnected", user=self.user)
pub.sendMessage("twitter.stream_connected", session_name=self.session_name)
def on_exception(self, ex):
log.exception("Exception received on streaming endpoint for user {}".format(self.user))
log.exception("Exception received on streaming endpoint for session {}".format(self.session_name))
def on_status(self, status):
""" Checks data arriving as a tweet. """
# Hide replies to users not followed by current account.
if status.in_reply_to_user_id_str != None and status.in_reply_to_user_id_str not in self.users and status.user.screen_name != self.user:
if status.in_reply_to_user_id_str != None and status.in_reply_to_user_id_str not in self.users and status.user.id != self.user_id:
return
if status.user.id_str in self.users:
pub.sendMessage("newStatus", status=status, user=self.user)
pub.sendMessage("twitter.new_status", status=status, session_name=self.session_name)

View File

@@ -231,7 +231,7 @@ def filter_tweet(tweet, tweet_data, settings, buffer_name):
if word not in getattr(tweet, value):
return False
if settings["filters"][i]["in_lang"] == "True":
if getattr(tweet, lang) not in settings["filters"][i]["languages"]:
if getattr(tweet, "lang") not in settings["filters"][i]["languages"]:
return False
elif settings["filters"][i]["in_lang"] == "False":
if tweet.lang in settings["filters"][i]["languages"]:

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import wx
class authorisationDialog(wx.Dialog):

View File

@@ -111,7 +111,8 @@ class URLStream(object):
self.prepared = False
log.debug("URL Player initialized")
# LibVLC controls.
self.instance = vlc.Instance()
self.instance = vlc.Instance("--quiet")
self.instance.log_unset()
self.player = self.instance.media_player_new()
self.event_manager = self.player.event_manager()
self.event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, self.end_callback)

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
import sys
import types
import pytest
import os
import sqlitedict
import shutil
from unittest import mock
# Mock sound module, so LibVLc won't complain.
sound_module = types.ModuleType("sound")
sys.modules["sound"] = sound_module
sound_module.soundManager = mock.MagicMock(name="sound.soundManager")
from sessions import base
# path where we will save our test config, as we can't rely on paths module due to pytest's paths being different.
session_path = os.path.join(os.getcwd(), "config", "testing")
@pytest.fixture
def session():
""" Configures a fake base session from where we can test things. """
global session_path
s = base.baseSession("testing")
if os.path.exists(session_path) == False:
os.mkdir(session_path)
# Patches paths.app_path and paths.config_path, so we will not have issues during session configuration.
with mock.patch("paths.app_path", return_value=os.getcwd()) as app_path:
with mock.patch("paths.config_path", return_value=os.path.join(os.getcwd(), "config")) as config_path:
s.get_configuration()
yield s
# Session's cleanup code.
if os.path.exists(session_path):
shutil.rmtree(session_path)
del s
@pytest.fixture
def dataset():
""" Generates a sample dataset"""
dataset = dict(home_timeline=["message" for i in range(10000)], mentions_timeline=["mention" for i in range(20000)])
yield dataset
### Testing database being read from disk.
def test_cache_in_disk_unlimited_size(session, dataset):
""" Tests cache database being read from disk, storing the whole datasets. """
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = -1
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 10000
assert len(session.db.get("mentions_timeline")) == 20000
session.db.close()
def test_cache_in_disk_limited_dataset(session, dataset):
""" Tests wether the cache stores only the amount of items we ask it to store. """
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = 100
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 100
assert len(session.db.get("mentions_timeline")) == 100
session.db.close()
def test_cache_in_disk_limited_dataset_unreversed(session):
"""Test if the cache is saved properly in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = 10
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 10
assert session.db.get("mentions_timeline")[0] == 10
assert session.db.get("home_timeline")[-1] == 19
assert session.db.get("mentions_timeline")[-1] == 19
session.db.close()
def test_cache_in_disk_limited_dataset_reversed(session):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = 10
session.settings["general"]["reverse_timelines"] = True
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 19
assert session.db.get("mentions_timeline")[0] == 19
assert session.db.get("home_timeline")[-1] == 10
assert session.db.get("mentions_timeline")[-1] == 10
session.db.close()
### Testing database being loaded into memory. Those tests should give the same results than before
### but as we have different code depending whether we load db into memory or read it from disk,
### We need to test this anyways.
def test_cache_in_memory_unlimited_size(session, dataset):
""" Tests cache database being loaded in memory, storing the whole datasets. """
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = -1
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 10000
assert len(session.db.get("mentions_timeline")) == 20000
def test_cache_in_memory_limited_dataset(session, dataset):
""" Tests wether the cache stores only the amount of items we ask it to store, when loaded in memory. """
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = 100
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 100
assert len(session.db.get("mentions_timeline")) == 100
def test_cache_in_memory_limited_dataset_unreversed(session):
"""Test if the cache is saved properly when loaded in memory in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = 10
session.load_persistent_data()
assert len(session.db)==1
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 10
assert session.db.get("mentions_timeline")[0] == 10
assert session.db.get("home_timeline")[-1] == 19
assert session.db.get("mentions_timeline")[-1] == 19
def test_cache_in_memory_limited_dataset_reversed(session):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. This test if for db read into memory. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = 10
session.settings["general"]["reverse_timelines"] = True
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 19
assert session.db.get("mentions_timeline")[0] == 19
assert session.db.get("home_timeline")[-1] == 10
assert session.db.get("mentions_timeline")[-1] == 10

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import pytest
from tweepy.models import Status
@pytest.fixture
def basic_tweet():
data = {'created_at': 'Mon Jan 03 15:03:36 +0000 2022', 'id': 1478019218884857856, 'id_str': '1478019218884857856', 'full_text': 'Changes in projects for next year https://t.co/nW3GS9RmHd', 'truncated': False, 'display_text_range': [0, 57], 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [], 'urls': [{'url': 'https://t.co/nW3GS9RmHd', 'expanded_url': 'https://manuelcortez.net/blog/changes-in-projects-for-next-year/#.YdMQQU6t1FI.twitter', 'display_url': 'manuelcortez.net/blog/changes-i…', 'indices': [34, 57]}]}, 'source': '<a href="https://mobile.twitter.com" rel="nofollow">Twitter Web App</a>', 'in_reply_to_status_id': None, 'in_reply_to_status_id_str': None, 'in_reply_to_user_id': None, 'in_reply_to_user_id_str': None, 'in_reply_to_screen_name': None, 'user': {'id': 258677951, 'id_str': '258677951', 'name': 'Manuel Cortez', 'screen_name': 'manuelcortez00', 'location': 'Nuevo León, México', 'description': 'Python developer, , interested in reading, accessibility, astronomy, physics and science. Я учу русский.', 'url': 'https://t.co/JFRKRA73ZV', 'entities': {'url': {'urls': [{'url': 'https://t.co/JFRKRA73ZV', 'expanded_url': 'https://manuelcortez.net', 'display_url': 'manuelcortez.net', 'indices': [0, 23]}]}, 'description': {'urls': []}}, 'protected': False, 'followers_count': 1453, 'friends_count': 568, 'listed_count': 45, 'created_at': 'Mon Feb 28 06:52:48 +0000 2011', 'favourites_count': 283, 'utc_offset': None, 'time_zone': None, 'geo_enabled': True, 'verified': False, 'statuses_count': 43371, 'lang': None, 'contributors_enabled': False, 'is_translator': False, 'is_translation_enabled': False, 'profile_background_color': 'C0DEED', 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 'profile_background_tile': False, 'profile_image_url': 'http://pbs.twimg.com/profile_images/442466677645508608/3EBBC-OX_normal.jpeg', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/442466677645508608/3EBBC-OX_normal.jpeg', 'profile_image_extensions_alt_text': None, 'profile_link_color': '1DA1F2', 'profile_sidebar_border_color': 'C0DEED', 'profile_sidebar_fill_color': 'DDEEF6', 'profile_text_color': '333333', 'profile_use_background_image': True, 'has_extended_profile': False, 'default_profile': True, 'default_profile_image': False, 'following': False, 'follow_request_sent': False, 'notifications': False, 'translator_type': 'regular', 'withheld_in_countries': []}, 'geo': None, 'coordinates': None, 'place': None, 'contributors': None, 'is_quote_status': False, 'retweet_count': 6, 'favorite_count': 2, 'favorited': False, 'retweeted': False, 'possibly_sensitive': False, 'possibly_sensitive_appealable': False, 'lang': 'en'}
yield Status().parse(api=None, json=data)
@pytest.fixture
def basic_tweet_multiple_mentions():
data = {'created_at': 'Mon Dec 27 21:21:25 +0000 2021', 'id': 1475577584947707909, 'id_str': '1475577584947707909', 'full_text': '@tamaranatalia9 @Darkstrings @Chris88171572 @manuelcortez00 Well done, thanks Tamara', 'truncated': False, 'display_text_range': [60, 84], 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [{'screen_name': 'tamaranatalia9', 'name': 'Tamara', 'id': 914114584591597568, 'id_str': '914114584591597568', 'indices': [0, 15]}, {'screen_name': 'Darkstrings', 'name': 'Luc', 'id': 1374154151115042823, 'id_str': '1374154151115042823', 'indices': [16, 28]}, {'screen_name': 'Chris88171572', 'name': 'Chris', 'id': 1323980014799495168, 'id_str': '1323980014799495168', 'indices': [29, 43]}, {'screen_name': 'manuelcortez00', 'name': 'Manuel Cortez', 'id': 258677951, 'id_str': '258677951', 'indices': [44, 59]}], 'urls': []}, 'source': '<a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>', 'in_reply_to_status_id': 1475550502083563526, 'in_reply_to_status_id_str': '1475550502083563526', 'in_reply_to_user_id': 914114584591597568, 'in_reply_to_user_id_str': '914114584591597568', 'in_reply_to_screen_name': 'tamaranatalia9', 'user': {'id': 784837522157436929, 'id_str': '784837522157436929', 'name': 'Paulus', 'screen_name': 'PauloPer01', 'location': '', 'description': '', 'url': None, 'entities': {'description': {'urls': []}}, 'protected': False, 'followers_count': 1082, 'friends_count': 3029, 'listed_count': 2, 'created_at': 'Sat Oct 08 19:27:01 +0000 2016', 'favourites_count': 78862, 'utc_offset': None, 'time_zone': None, 'geo_enabled': False, 'verified': False, 'statuses_count': 4976, 'lang': None, 'contributors_enabled': False, 'is_translator': False, 'is_translation_enabled': False, 'profile_background_color': 'F5F8FA', 'profile_background_image_url': None, 'profile_background_image_url_https': None, 'profile_background_tile': False, 'profile_image_url': 'http://pbs.twimg.com/profile_images/1464572633014587395/246oPPLa_normal.jpg', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1464572633014587395/246oPPLa_normal.jpg', 'profile_image_extensions_alt_text': None, 'profile_link_color': '1DA1F2', 'profile_sidebar_border_color': 'C0DEED', 'profile_sidebar_fill_color': 'DDEEF6', 'profile_text_color': '333333', 'profile_use_background_image': True, 'has_extended_profile': True, 'default_profile': True, 'default_profile_image': False, 'following': False, 'follow_request_sent': False, 'notifications': False, 'translator_type': 'none', 'withheld_in_countries': []}, 'geo': None, 'coordinates': None, 'place': None, 'contributors': None, 'is_quote_status': False, 'retweet_count': 1, 'favorite_count': 2, 'favorited': False, 'retweeted': False, 'lang': 'en'}
yield Status().parse(api=None, json=data)

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import pytest
import gettext
import datetime
gettext.install("test")
from unittest import mock
from sessions.twitter import templates
def test_default_values():
""" Tests wheter default values are the expected ones.
This might be useful so we will have this failing when we update anything from those values.
As TWBlue might be using those from other dialogs.
"""
assert templates.tweet_variables == ["date", "display_name", "screen_name", "source", "lang", "text", "image_descriptions"]
assert templates.dm_variables == ["date", "sender_display_name", "sender_screen_name", "recipient_display_name", "recipient_display_name", "text"]
assert templates.person_variables == ["display_name", "screen_name", "location", "description", "followers", "following", "listed", "likes", "tweets", "created_at"]
@pytest.mark.parametrize("offset, language, expected_result", [
(0, "en_US", "Wednesday, October 10, 2018 20:19:24"),
(-21600, "en_US", "Wednesday, October 10, 2018 14:19:24"),
(7200, "en_US", "Wednesday, October 10, 2018 22:19:24"),
(0, "es_ES", "miércoles, octubre 10, 2018 20:19:24"),
(-21600, "es_ES", "miércoles, octubre 10, 2018 14:19:24"),
(7200, "es_ES", "miércoles, octubre 10, 2018 22:19:24"),
(18000, "es_ES", "jueves, octubre 11, 2018 1:19:24"),
])
def test_process_date_absolute_time(offset, language, expected_result):
""" Tests date processing function for tweets, when relative_times is set to False. """
# Date representation used by twitter, converted to datetime object, as tweepy already does this.
# Original date was Wed Oct 10 20:19:24 +0000 2018
date_field = datetime.datetime(2018, 10, 10, 20, 19, 24)
with mock.patch("languageHandler.curLang", new=language):
processed_date = templates.process_date(date_field, relative_times=False, offset_seconds=offset)
assert processed_date == expected_result
def test_process_date_relative_time():
date_field = datetime.datetime(2018, 10, 10, 20, 19, 24)
with mock.patch("languageHandler.curLang", new="es_ES"):
processed_date = templates.process_date(date_field, relative_times=True, offset_seconds=7200)
# As this depends in relative times and this is subject to change, let's do some light checks here and hope the string is going to be valid.
assert isinstance(processed_date, str)
assert "hace" in processed_date and "años" in processed_date
def test_process_text_basic_tweet(basic_tweet):
expected_result = "Changes in projects for next year https://manuelcortez.net/blog/changes-in-projects-for-next-year/#.YdMQQU6t1FI.twitter"
text = templates.process_text(basic_tweet)
assert text == expected_result
def test_process_text_basic_tweet_multiple_mentions(basic_tweet_multiple_mentions):
expected_result = "@tamaranatalia9, @Darkstrings and 2 more: Well done, thanks Tamara"
text = templates.process_text(basic_tweet_multiple_mentions)
assert text == expected_result

View File

@@ -1,200 +0,0 @@
# -*- coding: utf-8 -*-
""" Test case to check some of the scenarios we might face when storing tweets in cache, both loading into memory or rreading from disk. """
import unittest
import os
import paths
import sqlitedict
import shutil
# The base session module requires sound as a dependency, and this needs libVLC to be locatable.
os.environ['PYTHON_VLC_MODULE_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", "x86"))
os.environ['PYTHON_VLC_LIB_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", "x86", "libvlc.dll"))
from sessions import base
class cacheTestCase(unittest.TestCase):
def setUp(self):
""" Configures a fake session to check caching objects here. """
self.session = base.baseSession("testing")
if os.path.exists(os.path.join(paths.config_path(), "testing")) == False:
os.mkdir(os.path.join(paths.config_path(), "testing"))
self.session.get_configuration()
def tearDown(self):
""" Removes the previously configured session. """
session_folder = os.path.join(paths.config_path(), "testing")
if os.path.exists(session_folder):
shutil.rmtree(session_folder)
def generate_dataset(self):
""" Generates a sample dataset"""
dataset = dict(home_timeline=["message" for i in range(10000)], mentions_timeline=["mention" for i in range(20000)])
return dataset
### Testing database being read from disk.
def test_cache_in_disk_unlimited_size(self):
""" Tests cache database being read from disk, storing the whole datasets. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = -1
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 10000)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 20000)
self.session.db.close()
def test_cache_in_disk_limited_dataset(self):
""" Tests wether the cache stores only the amount of items we ask it to store. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = 100
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 100)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 100)
self.session.db.close()
def test_cache_in_disk_limited_dataset_unreversed(self):
"""Test if the cache is saved properly in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = 10
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 10)
self.assertEquals(self.session.db.get("home_timeline")[-1], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 19)
self.session.db.close()
def test_cache_in_disk_limited_dataset_reversed(self):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = 10
self.session.settings["general"]["reverse_timelines"] = True
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 19)
self.assertEquals(self.session.db.get("home_timeline")[-1], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 10)
self.session.db.close()
### Testing database being loaded into memory. Those tests should give the same results than before
### but as we have different code depending whether we load db into memory or read it from disk,
### We need to test this anyways.
def test_cache_in_memory_unlimited_size(self):
""" Tests cache database being loaded in memory, storing the whole datasets. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = -1
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 10000)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 20000)
def test_cache_in_memory_limited_dataset(self):
""" Tests wether the cache stores only the amount of items we ask it to store, when loaded in memory. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = 100
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 100)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 100)
def test_cache_in_memory_limited_dataset_unreversed(self):
"""Test if the cache is saved properly when loaded in memory in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = 10
self.session.load_persistent_data()
self.assertTrue(len(self.session.db)==1)
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 10)
self.assertEquals(self.session.db.get("home_timeline")[-1], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 19)
def test_cache_in_memory_limited_dataset_reversed(self):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. This test if for db read into memory. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = 10
self.session.settings["general"]["reverse_timelines"] = True
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 19)
self.assertEquals(self.session.db.get("home_timeline")[-1], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 10)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,13 +0,0 @@
from __future__ import unicode_literals
import glob
import os.path
import platform
def find_datafiles():
system = platform.system()
if system == 'Windows':
file_ext = '*.exe'
else:
file_ext = '*.sh'
path = os.path.abspath(os.path.join(__path__[0], 'bootstrappers', file_ext))
return [('', glob.glob(path))]

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import application
from . import update
import platform
import logging
import output
from requests.exceptions import ConnectionError

Some files were not shown because too many files have changed in this diff Show More