├── .gitignore ├── orkiv ├── __main__.py ├── LQDN.png ├── icons │ ├── xa.png │ ├── away.png │ ├── offline.png │ └── available.png ├── customicon.png ├── sounds │ └── in.wav ├── sleekxmpp_buddylist.py ├── orkiv.kv └── main.py ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sublime-* 3 | -------------------------------------------------------------------------------- /orkiv/__main__.py: -------------------------------------------------------------------------------- 1 | from main import main 2 | main() 3 | -------------------------------------------------------------------------------- /orkiv/LQDN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srugano/kivy_watsapp/HEAD/orkiv/LQDN.png -------------------------------------------------------------------------------- /orkiv/icons/xa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srugano/kivy_watsapp/HEAD/orkiv/icons/xa.png -------------------------------------------------------------------------------- /orkiv/customicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srugano/kivy_watsapp/HEAD/orkiv/customicon.png -------------------------------------------------------------------------------- /orkiv/icons/away.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srugano/kivy_watsapp/HEAD/orkiv/icons/away.png -------------------------------------------------------------------------------- /orkiv/sounds/in.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srugano/kivy_watsapp/HEAD/orkiv/sounds/in.wav -------------------------------------------------------------------------------- /orkiv/icons/offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srugano/kivy_watsapp/HEAD/orkiv/icons/offline.png -------------------------------------------------------------------------------- /orkiv/icons/available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srugano/kivy_watsapp/HEAD/orkiv/icons/available.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{2014}}} {{{RUGANO Allan Stockman}}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | # README # 3 | 4 | I'm learning to build a **XMPP** app as a [Kivy](http://kivy.org/#home) application using this tutorial from [Dusty's Diverse Domain](http://archlinux.me/dusty) 5 | 6 | ####Screenshot#### 7 | 8 | ![XMPP client used with La Quadrature du Net in Kirundi](./orkiv/LQDN.png?raw=true "XMPP client used with La Quadrature du Net in Kirundi") 9 | 10 | ### What is this repository for? ### 11 | 12 | * [Kivy](http://kivy.org/#home) is an Open source Python library for rapid development of applications 13 | that make use of innovative user interfaces, such as multi-touch apps. 14 | * [XMPP](http://xmpp.org/about-xmpp/history/) is the formalization of the base XML streaming protocols for instant messaging and presence developed within the Jabber community. 15 | * Version 1 16 | * This README was written using [Markdown](https://bitbucket.org/tutorials/markdowndemo) 17 | 18 | ### How do I get set up? ### 19 | 20 | * Setting up a development environement and other dependencies are well explained on this [Dusty's post](http://archlinux.me/dusty/2014/04/25/creating-apps-in-kivy-the-book/) and on the [Kivy.org ](http://kivy.org/docs/gettingstarted/installation.html) installation page. 21 | 22 | ### Contribution guidelines ### 23 | 24 | * Writing tests 25 | * Code review 26 | * Other guidelines 27 | 28 | ### Who do I talk to? ### 29 | 30 | * iMitwe at @iMitwe 31 | * Github repo at [https://github.com/srugano](https://github.com/srugano) 32 | -------------------------------------------------------------------------------- /orkiv/sleekxmpp_buddylist.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sleekxmpp import ClientXMPP 4 | from sleekxmpp.exceptions import IqError, IqTimeout 5 | 6 | 7 | class EchoBot(ClientXMPP): 8 | 9 | def __init__(self, jid, password): 10 | ClientXMPP.__init__(self, jid, password) 11 | 12 | self.add_event_handler("session_start", self.session_start) 13 | self.add_event_handler("message", self.message) 14 | 15 | # If you wanted more functionality, here's how to register plugins: 16 | # self.register_plugin('xep_0030') # Service Discovery 17 | # self.register_plugin('xep_0199') # XMPP Ping 18 | 19 | # Here's how to access plugins once you've registered them: 20 | # self['xep_0030'].add_feature('echo_demo') 21 | 22 | # If you are working with an OpenFire server, you will 23 | # need to use a different SSL version: 24 | # import ssl 25 | # self.ssl_version = ssl.PROTOCOL_SSLv3 26 | 27 | def session_start(self, event): 28 | self.send_presence() 29 | self.get_roster() 30 | 31 | # Most get_*/set_* methods from plugins use Iq stanzas, which 32 | # can generate IqError and IqTimeout exceptions 33 | # 34 | # try: 35 | # self.get_roster() 36 | # except IqError as err: 37 | # logging.error('There was an error getting the roster') 38 | # logging.error(err.iq['error']['condition']) 39 | # self.disconnect() 40 | # except IqTimeout: 41 | # logging.error('Server is taking too long to respond') 42 | # self.disconnect() 43 | 44 | def message(self, msg): 45 | if msg['type'] in ('chat', 'normal'): 46 | msg.reply("Thanks for sending\n%(body)s" % msg).send() 47 | 48 | 49 | if __name__ == '__main__': 50 | # Ideally use optparse or argparse to get JID, 51 | # password, and log level. 52 | 53 | logging.basicConfig(level=logging.DEBUG, 54 | format='%(levelname)-8s %(message)s') 55 | 56 | xmpp = EchoBot('somejid@example.com', 'use_getpass') 57 | xmpp.connect() 58 | xmpp.process(block=True) 59 | -------------------------------------------------------------------------------- /orkiv/orkiv.kv: -------------------------------------------------------------------------------- 1 | #:import la kivy.adapters.listadapter 2 | #:import ok main 3 | 4 | OrkivRoot: 5 | 6 | : 7 | mode: "narrow" if self.width < dp(600) else "wide" 8 | AccountDetailsForm: 9 | 10 | : 11 | anchor_y: "top" 12 | server_box: server_input 13 | username_box: username_input 14 | password_box: password_input 15 | BoxLayout: 16 | orientation: "vertical" 17 | height: "200dp" 18 | size_hint_y: None 19 | GridLayout: 20 | cols: 2 21 | row_default_height: "40dp" 22 | row_force_default: True 23 | spacing: "10dp" 24 | padding: "10dp" 25 | Label: 26 | text: "Server" 27 | AccountDetailsTextInput: 28 | id: server_input 29 | next: username_input 30 | Label: 31 | text: "Itazirano" 32 | AccountDetailsTextInput: 33 | id: username_input 34 | next: password_input 35 | Label: 36 | text: "Modopases" 37 | AccountDetailsTextInput: 38 | next: server_input 39 | password: True 40 | id: password_input 41 | Button: 42 | size_hint_y: None 43 | height: "40dp" 44 | text: "Ikonekte" 45 | on_press: root.login() 46 | 47 | : 48 | size_hint_y: None 49 | height: "75dp" 50 | selected_color: (0.2, 0.1, 0.2, 1.0) 51 | on_release: app.root.show_buddy_chat(self.jabberid) 52 | BoxLayout: 53 | size_hint_x: 3 54 | orientation: 'vertical' 55 | Label: 56 | text: root.jabberid 57 | font_size: "25dp" 58 | size_hint_y: 0.7 59 | Label: 60 | text: root.status_message 61 | color: (0.5, 0.5, 0.5, 1.0) 62 | font_size: "15dp" 63 | size_hint_y: 0.3 64 | Label: 65 | text: root.jabberid 66 | Label: 67 | text: root.full_name 68 | Image: 69 | source: app.root_dir + "icons/" + root.online_status + ".png" 70 | 71 | : 72 | list_view: list_view 73 | ListView: 74 | id: list_view 75 | adapter: la.ListAdapter(data=[], cls=ok.BuddyListItem, args_converter=root.roster_converter) 76 | 77 | : 78 | orientation: "vertical" 79 | chat_log_label: chat_log_label 80 | send_chat_textinput: send_chat_textinput 81 | Label: 82 | text: "terinkuru na " + root.jabber_id 83 | size_hint_y: None 84 | height: "40dp" 85 | ScrollView: 86 | Label: 87 | size_hint_y: None 88 | text_size: (root.width, None) 89 | size: self.texture_size 90 | markup: True 91 | id: chat_log_label 92 | BoxLayout: 93 | size_hint_y: None 94 | height: "50dp" 95 | Button: 96 | size_hint_x: None 97 | width: "70dp" 98 | text: "Abagenzi" 99 | on_release: app.root.show_buddy_list() 100 | EnterTextInput: 101 | id: send_chat_textinput 102 | on_enter_key: root.send_message() 103 | Button: 104 | size_hint_x: None 105 | width: "70dp" 106 | text: "Rungika" 107 | on_release: root.send_message() -------------------------------------------------------------------------------- /orkiv/main.py: -------------------------------------------------------------------------------- 1 | from kivy.app import App 2 | from kivy.uix.anchorlayout import AnchorLayout 3 | from kivy.properties import ObjectProperty 4 | from sleekxmpp import ClientXMPP 5 | from kivy.uix.textinput import TextInput 6 | from kivy.uix.modalview import ModalView 7 | from kivy.uix.label import Label 8 | from sleekxmpp.exceptions import XMPPError 9 | from sleekxmpp.jid import InvalidJID 10 | from kivy.uix.button import Button 11 | from kivy.uix.boxlayout import BoxLayout 12 | from kivy.properties import StringProperty 13 | from kivy.uix.listview import ListItemButton 14 | import datetime 15 | from kivy.utils import escape_markup 16 | from kivy.core.audio import SoundLoader 17 | 18 | 19 | __version__ = 1.0 20 | 21 | class Orkiv(App): 22 | icon = 'customicon.png' 23 | 24 | def __init__(self, root_dir): 25 | super(Orkiv, self).__init__() 26 | self.root_dir = root_dir 27 | self.xmpp = None 28 | 29 | def connect_to_jabber(self, jabber_id, password): 30 | self.xmpp = ClientXMPP(jabber_id, password) 31 | self.xmpp.reconnect_max_attempts = 1 32 | connected = self.xmpp.connect() 33 | if not connected: 34 | raise XMPPError("Vyanse kwikonecta") 35 | self.xmpp.process() 36 | self.xmpp.send_presence() 37 | self.xmpp.get_roster() 38 | self.xmpp.add_event_handler('message', self.root.handle_xmpp_message) 39 | 40 | def disconnect_xmpp(self): 41 | if self.xmpp and self.xmpp.state.ensure("connected"): 42 | self.xmpp.abort() 43 | self.xmpp = None 44 | 45 | def on_stop(self): 46 | self.disconnect_xmpp() 47 | 48 | class AccountDetailsForm(AnchorLayout): 49 | server_box = ObjectProperty() 50 | username_box = ObjectProperty() 51 | password_box = ObjectProperty() 52 | 53 | def login(self): 54 | jabber_id = self.username_box.text + "@" + self.server_box.text 55 | modal = ConnectionModal(jabber_id, self.password_box.text) 56 | modal.open() 57 | 58 | class EnterTextInput(TextInput): 59 | def __init__(self, **kwargs): 60 | self.register_event_type("on_enter_key") 61 | super(EnterTextInput, self).__init__(**kwargs) 62 | 63 | def _keyboard_on_key_down(self, window, keycode, text, modifiers): 64 | if keycode[0] == 13: # 13 is the keycode for 65 | self.dispatch("on_enter_key") 66 | else: 67 | super(EnterTextInput, self)._keyboard_on_key_down( 68 | window, keycode, text, modifiers) 69 | 70 | 71 | def on_enter_key(self): 72 | pass 73 | 74 | class AccountDetailsTextInput(EnterTextInput): 75 | next = ObjectProperty() 76 | 77 | def _keyboard_on_key_down(self, window, keycode, text, modifiers): 78 | if keycode[0] == 9: # 9 is the keycode for 79 | self.next.focus = True 80 | else: 81 | super(AccountDetailsTextInput, self)._keyboard_on_key_down( 82 | window, keycode, text, modifiers) 83 | 84 | def on_enter_key(self): 85 | self.parent.parent.parent.login() # this is not future friendly 86 | 87 | class ConnectionModal(ModalView): 88 | def __init__(self, jabber_id, password): 89 | super(ConnectionModal, self).__init__(auto_dismiss=False, 90 | anchor_y="bottom") 91 | self.label = Label(text="Turiko turikonekta kuri %s..." % jabber_id) 92 | self.add_widget(self.label) 93 | self.jabber_id = jabber_id 94 | self.password = password 95 | self.on_open = self.connect_to_jabber 96 | 97 | def connect_to_jabber(self): 98 | app = Orkiv.get_running_app() 99 | try: 100 | app.connect_to_jabber(self.jabber_id, self.password) 101 | app.root.show_buddy_list() 102 | self.dismiss() 103 | except (XMPPError, InvalidJID): 104 | self.label.text = "Mutubabarire, vyanse ko twikonekta, gerageza murabe ibisabwa" 105 | button = Button(text="Gerageza kandi") 106 | button.size_hint = (1.0, None) 107 | button.height = "40dp" 108 | button.bind(on_press=self.dismiss) 109 | self.add_widget(button) 110 | app.disconnect_xmpp() 111 | 112 | class BuddyList(BoxLayout): 113 | list_view = ObjectProperty() 114 | 115 | def __init__(self): 116 | super(BuddyList, self).__init__() 117 | self.app = Orkiv.get_running_app() 118 | self.list_view.adapter.data = sorted(self.app.xmpp.client_roster.keys()) 119 | self.new_messages = set() 120 | 121 | def force_list_view_update(self): 122 | self.list_view.adapter.update_for_new_data() 123 | self.list_view._trigger_reset_populate() 124 | 125 | def roster_converter(self, index, jabberid): 126 | result = { 127 | "jabberid": jabberid, 128 | "full_name": self.app.xmpp.client_roster[jabberid]['name'] 129 | } 130 | 131 | presence = sorted( 132 | self.app.xmpp.client_roster.presence(jabberid).values(), 133 | key=lambda p: p.get("priority", 100), reverse=True) 134 | 135 | if presence: 136 | result['status_message'] = presence[0].get('status', '') 137 | show = presence[0].get('show') 138 | result['online_status'] = show if show else "available" 139 | else: 140 | result['status_message'] = "" 141 | result['online_status'] = "offline" 142 | 143 | if jabberid in self.new_messages: 144 | result['background_color'] = (0.6, 0.4, 0.6, 1) 145 | else: 146 | result['background_color'] = (0, 0, 0, 1) 147 | if index % 2: 148 | result['background_color'] = (x + .3 for x in result['background_color']) 149 | 150 | return result 151 | 152 | class ChatWindow(BoxLayout): 153 | jabber_id = StringProperty() 154 | chat_log_label = ObjectProperty() 155 | send_chat_textinput = ObjectProperty() 156 | 157 | def append_chat_message(self, sender, message, color): 158 | self.chat_log_label.text += "[b](%s) [color=%s]%s[/color][/b]: %s\n" % ( 159 | datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 160 | color, 161 | escape_markup(sender), 162 | escape_markup(message)) 163 | self.chat_log_label.parent.scroll_y = 0.0 164 | 165 | def send_message(self): 166 | app = Orkiv.get_running_app() 167 | app.xmpp.send_message( 168 | mto=self.jabber_id, 169 | mbody=self.send_chat_textinput.text) 170 | self.append_chat_message("Jewe:", self.send_chat_textinput.text, color="aaffbb") 171 | self.send_chat_textinput.text = '' 172 | 173 | class OrkivRoot(BoxLayout): 174 | 175 | mode = StringProperty("narrow") 176 | 177 | @property 178 | def chat_visible(self): 179 | return ChatWindow in {c.__class__ for c in self.children} 180 | 181 | @property 182 | def buddy_list_visible(self): 183 | return self.buddy_list in self.children 184 | 185 | def __init__(self): 186 | super(OrkivRoot, self).__init__() 187 | self.buddy_list = None 188 | self.chat_windows = {} 189 | self.in_sound = SoundLoader.load("orkiv/sounds/in.wav") 190 | 191 | def show_buddy_list(self): 192 | self.clear_widgets() 193 | if not self.buddy_list: 194 | self.buddy_list = BuddyList() 195 | for buddy_list_items in self.buddy_list.list_view.adapter.selection: 196 | buddy_list_items.deselect() 197 | self.add_widget(self.buddy_list) 198 | 199 | def get_chat_window(self, jabber_id): 200 | if jabber_id not in self.chat_windows: 201 | self.chat_windows[jabber_id] = ChatWindow(jabber_id=jabber_id) 202 | return self.chat_windows[jabber_id] 203 | 204 | def show_buddy_chat(self, jabber_id): 205 | self.clear_widgets() 206 | if self.mode == "wide": 207 | self.add_widget(self.buddy_list) 208 | 209 | self.add_widget(self.get_chat_window(jabber_id)) 210 | self.buddy_list.new_messages.discard(jabber_id) 211 | self.buddy_list.force_list_view_update() 212 | 213 | def handle_xmpp_message(self, message): 214 | if message['type'] not in ['normal', 'chat']: 215 | return 216 | jabber_id = message['from'].bare 217 | 218 | chat_window = self.get_chat_window(jabber_id) 219 | chat_window.append_chat_message(jabber_id, message['body'], color="aaaaff") 220 | self.in_sound.play() 221 | if chat_window not in self.children: 222 | self.buddy_list.new_messages.add(jabber_id) 223 | self.buddy_list.force_list_view_update() 224 | 225 | def on_mode(self, widget, mode): 226 | if mode == "narrow": 227 | if self.chat_visible and self.buddy_list_visible: 228 | self.remove_widget(self.buddy_list) 229 | else: 230 | if self.chat_visible and not self.buddy_list_visible: 231 | self.add_widget(self.buddy_list, index=1) 232 | 233 | class BuddyListItem(BoxLayout, ListItemButton): 234 | jabberid = StringProperty() 235 | full_name = StringProperty() 236 | status_message = StringProperty() 237 | online_status = StringProperty() 238 | background = ObjectProperty() 239 | 240 | def main(root_dir="orkiv/"): 241 | Orkiv(root_dir).run() 242 | 243 | if __name__ == "__main__": 244 | main() 245 | --------------------------------------------------------------------------------